Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java annotation processing API accessing import statements

I am writing an AnnotationProcessor which is supposed to generate java code. It should generate a derived interface from certain existing interfaces.

For this purpose I need to find the import statements of the original input code, so that I can output it in the generated java file.

How can this be done?

like image 823
Dennis Thrysøe Avatar asked Feb 06 '13 16:02

Dennis Thrysøe


3 Answers

You can't get import statements with an annotation processor. What you can get though, are the types used by that class, which is even better.

Import statements from the source code are not enough for analyzing what types are used in a class, because not all used types have import statements. If you really only need the actual statements, you could read the source file directly.

There are some issues if you only look at the statements:

  • fully qualified class name, e.g. a property java.util.Date date;
  • imports from the same package don't have explicit import statements
  • imports statements are declared for all classes in a file
  • unused import statements could cause additional confusion

With the annotation processor and the Mirror API, you can get the types of properties, method parameters, method return types, etc. - basically the types of every declaration that is not in a method or block. This should be good enough.

You should analyse every element of the class and store it's type in a Set. There are some utility classes that help with this task. You can ignore any type in the java.lang package since it is always implicitly imported.

A minimal annotation processor may look like this:

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("*")
public class Processor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        ImportScanner scanner = new ImportScanner();
        scanner.scan(roundEnv.getRootElements(), null);

        Set<String> importedTypes = scanner.getImportedTypes();
        // do something with the types

        return false;
    }

}

The Scanner here extends ElementScanner7 which is based on a visitor pattern. We only implement a few visitor methods and filter elements by kind because not all elements can actually contain importable types.

import java.util.HashSet;
import java.util.Set;

import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.ElementScanner7;

public class ImportScanner extends ElementScanner7<Void, Void> {

    private Set<String> types = new HashSet<>();

    public Set<String> getImportedTypes() {
        return types;
    }

    @Override
    public Void visitType(TypeElement e, Void p) {
        for(TypeMirror interfaceType : e.getInterfaces()) {
            types.add(interfaceType.toString());
        }
        types.add(e.getSuperclass().toString());
        return super.visitType(e, p);
    }

    @Override
    public Void visitExecutable(ExecutableElement e, Void p) {
        if(e.getReturnType().getKind() == TypeKind.DECLARED) {
            types.add(e.getReturnType().toString());
        }
        return super.visitExecutable(e, p);
    }

    @Override
    public Void visitTypeParameter(TypeParameterElement e, Void p) {
        if(e.asType().getKind() == TypeKind.DECLARED) {
            types.add(e.asType().toString());
        }
        return super.visitTypeParameter(e, p);
    }

    @Override
    public Void visitVariable(VariableElement e, Void p) {
        if(e.asType().getKind() == TypeKind.DECLARED) {
            types.add(e.asType().toString());
        }
        return super.visitVariable(e, p);
    }

}

This scanner returns a set of types as fully qualified paths. There are still a few things to consider and some things to implement:

  • The set contains elements from java.lang and also types from the same package
  • The set contains generics, like java.util.List<String>
  • TypeKind.DECLARED is not the only kind of elements that is an importable type. Also check TypeKind.ARRAY and get the actual declared type of it. Instead of adding another to else if(e.asType().getKind() == TypeKind.ARRAY) // ... the class TypeKindVisitor7 could be used instead
  • Depending on the use case there may be even more types to be discovered. For example, annotations can contain classes as arguments.
  • For Java 1.6 use the respective ElementScanner6, TypeKindVisitor6 etc. implementations.
like image 162
kapex Avatar answered Oct 16 '22 22:10

kapex


It looks like there is no way to get import statements from the standard SDK classes (at least with SDK 5-6-7).

Nevertheless, you can use some classes inside tools.jar from SUN/Oracle.

import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;

public class MyProcessor extends AbstractProcessor {

    @Override
    public void init(ProcessingEnvironment env) {
        tree = Trees.instance(env);
    }

    @Override
    public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnvironment) {
        for( Element rootElement : roundEnvironment.getRootElements() ) {
            TreePath path = tree.getPath(rootElement);
            System.out.println( "root element "+rootElement.toString() +" "+path.getCompilationUnit().getImports().size() );
        }
....

To get the jar of Java tools via maven, refer to this thread.

There should be an alternative using a TreePathScanner (from tools.jar as well) but the visitImport method was never triggered for me.

like image 27
Snicolas Avatar answered Oct 16 '22 21:10

Snicolas


<dependency>
    <groupId>org.jvnet.sorcerer</groupId>
    <artifactId>sorcerer-javac</artifactId>
    <version>0.8</version>
</dependency>

Using this dependency fix the problem, but unfortunately this time it supports up to Java 1.7 and you cannot correctly compile Java 1.8 source. My solution is a little bit hack, but it works without using this dependency and with Java 1.8 sources

public final class SorcererJavacUtils {
private static final Pattern IMPORT = Pattern.compile("import\\s+(?<path>[\\w\\\\.]+\\*?)\\s*;");

// com.sun.tools.javac.model.JavacElements
public static Set<String> getImports(Element element, ProcessingEnvironment processingEnv) {
    Elements elements = processingEnv.getElementUtils();
    Class<?> cls = elements.getClass();

    try {
        Method getTreeAndTopLevel = cls.getDeclaredMethod("getTreeAndTopLevel", Element.class);
        getTreeAndTopLevel.setAccessible(true);
        // Pair<JCTree, JCCompilationUnit>
        Object treeTop = getTreeAndTopLevel.invoke(elements, element);

        if (treeTop == null)
            return Collections.emptySet();

        // JCCompilationUnit
        Object toplevel = getFieldValue("snd", treeTop);

        return SorcererJavacUtils.<List<Object>>getFieldValue("defs", toplevel).stream()
                                                                               .map(Object::toString)
                                                                               .map(IMPORT::matcher)
                                                                               .filter(Matcher::find)
                                                                               .map(matcher -> matcher.group("path"))
                                                                               .collect(Collectors.toSet());
    } catch(Exception ignored) {
        return Collections.emptySet();
    }
}

private static <T> T getFieldValue(String name, Object obj) throws IllegalAccessException, NoSuchFieldException {
    Field field = obj.getClass().getDeclaredField(name);
    field.setAccessible(true);
    return (T)field.get(obj);
}

private SorcererJavacUtils() {
}

}

like image 1
oleg.cherednik Avatar answered Oct 16 '22 22:10

oleg.cherednik