8

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?

Dennis Thrysøe
  • 1,791
  • 4
  • 19
  • 31
  • Hi, I'm was found this question, maybe can be useful http://stackoverflow.com/questions/11385628/how-to-write-a-java-annotation-processor – Carlos Spohr Feb 06 '13 at 16:54

3 Answers3

9

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.
kapex
  • 28,903
  • 6
  • 107
  • 121
  • What about static imports ? Nice solution though. – Snicolas Sep 13 '13 at 08:46
  • @Snicolas Well, if you static import a class (which doesn't really make sense) and declare something of that type it will be included. But the usually static imported constants aren't visible to the processor. If they are compile time constants you can get their value, but you won't know where they are imported from. – kapex Sep 13 '13 at 09:46
  • Fine. And how would you compare your solution to mine. Is there any drawback to use SUN tools ? – Snicolas Sep 13 '13 at 13:53
  • `javax.annotation.processing` has the advantage of being implemented in the jdk. You don't need to include any jar. I think in the end it depends on the use case though, whether you need what's in the source or what the class actually uses. Annotation processing is more abstract while the ast tools are more low level in comparison. Their implementations seem similar; use of visitor patterns and at some point the compiler parses the source code. So you could also try to combine both approaches, get import statements and also visit types like in my example using the ast tools. – kapex Sep 13 '13 at 15:06
  • To clarify, tools.jar is part of oracle's JDK too, but [`javax.tools` doesn't provide an implementation](http://stackoverflow.com/a/1713968/897024). You also can't distribute the jar due to its license afaik. That said I'm not sure if there is any specification that a JDK must implement annotation processing at all too. – kapex Sep 13 '13 at 15:13
  • 1
    Well, definitly I am going to switch to your solution. Mine does work when using a standard JDK compiler, but it fails inside IDEs like Eclipse, and I guess others. The problem with my approach comes with Trees.newInstance that refuses the processing environment when it's not provided by a standard JDK compiler. You earned 100 points :) – Snicolas Sep 13 '13 at 19:03
  • BTW, I did use you implementation (or so) and it works like a charm. Thx. The project using it is here :https://github.com/stephanenicolas/boundbox – Snicolas Sep 17 '13 at 05:35
  • Thanks, I'm glad I could help! Interesting project, I'll keep an eye on it. – kapex Sep 17 '13 at 07:13
4

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.

Community
  • 1
  • 1
Snicolas
  • 37,840
  • 15
  • 114
  • 173
  • Thank you for this solution. I'm using the imports to resolve unqualified class names in Javadoc comments, and this fits the bill (though I'd love to use a standard API instead). – dnault Nov 05 '16 at 16:33
1
<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() {
}

}

Oleg Cherednik
  • 17,377
  • 4
  • 21
  • 35