Find out which classes of a given API are used

In a Java Project of mine, I would like to find out programmatically which classes from a given API are used. Is there a good way to do that? Through source code parsing or bytecode parsing maybe? Because Reflection won't be of any use, I'm afraid.

To make things simpler: there are no wildcard imports (import com.mycompany.api.*;) anywhere in my project, no fully qualified field or variable definitions (private com.mycompany.api.MyThingy thingy;) nor any Class.forName(...) constructs. Given these limitations, it boils down to parsing import statements, I guess. Is there a preferred approach to do this?


Solution 1:

You can discover the classes using ASM's Remapper class (believe it or not). This class is actually meant to replace all occurrences of a class name within bytecode. For your purposes, however, it doesn't need to replace anything.

This probably doesn't make a whole lot of sense, so here is an example...

First, you create a subclass of Remapper whose only purpose in life is to intercept all calls to the mapType(String) method, recording its argument for later use.

public class ClassNameRecordingRemapper extends Remapper {

    private final Set<? super String> classNames;

    public ClassNameRecordingRemapper(Set<? super String> classNames) {
        this.classNames = classNames;
    }

    @Override
    public String mapType(String type) {
        classNames.add(type);
        return type;
    }

}

Now you can write a method like this:

public Set<String> findClassNames(byte[] bytecode) {
    Set<String> classNames = new HashSet<String>();

    ClassReader classReader = new ClassReader(bytecode);
    ClassWriter classWriter = new ClassWriter(classReader, 0);

    ClassNameRecordingRemapper remapper = new ClassNameRecordingRemapper(classNames);
    classReader.accept(remapper, 0);

    return classNames;
}

It's your responsibility to actually obtain all classes' bytecode.


EDIT by seanizer (OP)

I am accepting this answer, but as the above code is not quite correct, I will insert the way I used this:

public static class Collector extends Remapper{

    private final Set<Class<?>> classNames;
    private final String prefix;

    public Collector(final Set<Class<?>> classNames, final String prefix){
        this.classNames = classNames;
        this.prefix = prefix;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String mapDesc(final String desc){
        if(desc.startsWith("L")){
            this.addType(desc.substring(1, desc.length() - 1));
        }
        return super.mapDesc(desc);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String[] mapTypes(final String[] types){
        for(final String type : types){
            this.addType(type);
        }
        return super.mapTypes(types);
    }

    private void addType(final String type){
        final String className = type.replace('/', '.');
        if(className.startsWith(this.prefix)){
            try{
                this.classNames.add(Class.forName(className));
            } catch(final ClassNotFoundException e){
                throw new IllegalStateException(e);
            }
        }
    }

    @Override
    public String mapType(final String type){
        this.addType(type);
        return type;
    }

}

public static Set<Class<?>> getClassesUsedBy(
    final String name,   // class name
    final String prefix  // common prefix for all classes
                         // that will be retrieved
    ) throws IOException{
    final ClassReader reader = new ClassReader(name);
    final Set<Class<?>> classes =
        new TreeSet<Class<?>>(new Comparator<Class<?>>(){

            @Override
            public int compare(final Class<?> o1, final Class<?> o2){
                return o1.getName().compareTo(o2.getName());
            }
        });
    final Remapper remapper = new Collector(classes, prefix);
    final ClassVisitor inner = new EmptyVisitor();
    final RemappingClassAdapter visitor =
        new RemappingClassAdapter(inner, remapper);
    reader.accept(visitor, 0);
    return classes;
}

Here's a main class to test it using:

public static void main(final String[] args) throws Exception{
    final Collection<Class<?>> classes =
        getClassesUsedBy(Collections.class.getName(), "java.util");
    System.out.println("Used classes:");
    for(final Class<?> cls : classes){
        System.out.println(" - " + cls.getName());
    }

}

And here's the Output:

Used classes:
 - java.util.ArrayList
 - java.util.Arrays
 - java.util.Collection
 - java.util.Collections
 - java.util.Collections$1
 - java.util.Collections$AsLIFOQueue
 - java.util.Collections$CheckedCollection
 - java.util.Collections$CheckedList
 - java.util.Collections$CheckedMap
 - java.util.Collections$CheckedRandomAccessList
 - java.util.Collections$CheckedSet
 - java.util.Collections$CheckedSortedMap
 - java.util.Collections$CheckedSortedSet
 - java.util.Collections$CopiesList
 - java.util.Collections$EmptyList
 - java.util.Collections$EmptyMap
 - java.util.Collections$EmptySet
 - java.util.Collections$ReverseComparator
 - java.util.Collections$ReverseComparator2
 - java.util.Collections$SelfComparable
 - java.util.Collections$SetFromMap
 - java.util.Collections$SingletonList
 - java.util.Collections$SingletonMap
 - java.util.Collections$SingletonSet
 - java.util.Collections$SynchronizedCollection
 - java.util.Collections$SynchronizedList
 - java.util.Collections$SynchronizedMap
 - java.util.Collections$SynchronizedRandomAccessList
 - java.util.Collections$SynchronizedSet
 - java.util.Collections$SynchronizedSortedMap
 - java.util.Collections$SynchronizedSortedSet
 - java.util.Collections$UnmodifiableCollection
 - java.util.Collections$UnmodifiableList
 - java.util.Collections$UnmodifiableMap
 - java.util.Collections$UnmodifiableRandomAccessList
 - java.util.Collections$UnmodifiableSet
 - java.util.Collections$UnmodifiableSortedMap
 - java.util.Collections$UnmodifiableSortedSet
 - java.util.Comparator
 - java.util.Deque
 - java.util.Enumeration
 - java.util.Iterator
 - java.util.List
 - java.util.ListIterator
 - java.util.Map
 - java.util.Queue
 - java.util.Random
 - java.util.RandomAccess
 - java.util.Set
 - java.util.SortedMap
 - java.util.SortedSet

Solution 2:

I think the following might help you out:

  1. Class Dependency Analyzer
  2. Dependency Finder

Solution 3:

  1. Compiler Tree API
  2. byte code analysis - the fully qualified names should be in the constant pool

Solution 4:

Something like this perhaps:

import java.io.*;
import java.util.Scanner;
import java.util.regex.Pattern;

public class FileTraverser {

    public static void main(String[] args) {
        visitAllDirsAndFiles(new File("source_directory"));
    }

    public static void visitAllDirsAndFiles(File root) {
        if (root.isDirectory())
            for (String child : root.list())
                visitAllDirsAndFiles(new File(root, child));
        process(root);
    }

    private static void process(File f) {

        Pattern p = Pattern.compile("(?=\\p{javaWhitespace}*)import (.*);");
        if (f.isFile() && f.getName().endsWith(".java")) {
            try {
                Scanner s = new Scanner(f);
                String cls = "";
                while (null != (cls = s.findWithinHorizon(p, 0)))
                    System.out.println(cls);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}

You may want to take comments into account, but it shouldn't be too hard. You could also make sure you only look for imports before the class declaration.