11

Question:

  1. Is it possible to access elements annotated with a @Target(ElementType.TYPE_USE) annotation via an annotation processor?
  2. Is it possible to access the annotated type bounds via an annotation processor?

Links to related documentation I missed are highly appreciated.

Context:

The annotation:

@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.SOURCE)
public @interface TypeUseAnno {}

An example class:

public class SomeClass extends HashMap<@TypeUseAnno String, String> {}

The processor:

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("base.annotations.TypeUseAnno")
public class Processor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Initialized.");
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Invoked.");
        for (TypeElement annotation : annotations) {
            this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "" + roundEnv.getElementsAnnotatedWith(annotation));
        }
        return true;
    }
}

Compiling the above SomeClass with Processor on the classpath will show the "Intialized" message but the process(...) method is never invoked. Adding another annotation to the processor with @Target(ElementType.PARAMETER) works fine when the annotation is present on a method parameter. If the method parameter is annotated with @TypeUseAnno the process will again ignore the element.

Community
  • 1
  • 1
Trinova
  • 443
  • 6
  • 18

1 Answers1

7

The TYPE_USE annotations are a bit tricky, because the compiler treats them differently, than the "old usage" annotations.

So as you correctly observed, they are not passed to annotation processor, and your process() method will never receive them.

So how to use them at compilation time?

In Java 8, where these annotations got introduced, there was also introduced new way to attach to java compilation. You can now attach listener to compilation tasks, and trigger your own traversal of the source code. So your task to access the annotation splits into two.

  1. Hook to the compiler.
  2. Implement your analyzer.

Ad 1. There are 2 options to hook on the compiler in Java 8:

  1. Using new compiler plugin API.
  2. Using annotation processor.

I haven't used option #1 much, because it needs to be explicitely specified as javac parameter. So I'll describe option #1:

You have to attach TaskListener to the propper compilation phase. There are various phases. Following one is the only one, during which you have accessible syntax tree representing full source code including method bodies (remember, that TYPE_USE annotations can be used even on local variable declarations.

@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class EndProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        Trees trees = Trees.instance(env);
        JavacTask.instance(env).addTaskListener(new TaskListener() {

            @Override
            public void started(TaskEvent taskEvent) {
                // Nothing to do on task started event.
            }

            @Override
            public void finished(TaskEvent taskEvent) {
                if(taskEvent.getKind() == ANALYZE) {
                    new MyTreeScanner(trees).scan(taskEvent.getCompilationUnit(), null);
                }
            }
            
        });
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // We don't care about this method, as it will never be invoked for our annotation.
        return false;
    }
}

Ad 2. Now the MyTreeScanner can scan the full source code, and find the annotations. That applies no matter if you used the Plugin or AnnotationProcessor approach. This is still tricky. You have to implement the TreeScanner, or typically extend the TreePathScanner. This represents a visitor pattern, where you have to properly analyze, which elements are of your interest to be visited.

Let's give simple example, that can somehow react on local variable declaration (give me 5 minutes):

class MyTreeScanner extends TreePathScanner<Void, Void> {
    private final Trees trees;

    public MyTreeScanner(Trees trees) {
        this.trees = trees;
    }

    @Override
    public Void visitVariable(VariableTree tree, Void aVoid) {
        super.visitVariable(variableTree, aVoid);
        // This method might be invoked in case of
        //  1. method field definition
        //  2. method parameter
        //  3. local variable declaration
        // Therefore you have to filter out somehow what you don't need.
        if(tree.getKind() == Tree.Kind.VARIABLE) {
            Element variable = trees.getElement(trees.getPath(getCurrentPath().getCompilationUnit(), tree));
            MyUseAnnotation annotation = variable.getAnnotation(MyUseAnnotation.class);
            // Here you have your annotation.
            // You can process it now.
        }
        return aVoid;
    }
}

This is very brief introduction. For real examples you can have a look at following project source code: https://github.com/c0stra/fluent-api-end-check/tree/master/src/main/java/fluent/api/processors

It's also very important to have good tests while developing such features, so you can debug, reverse engineer and solve all the tricky issues you'll face in this area ;) For that you can also get inspired here: https://github.com/c0stra/fluent-api-end-check/blob/master/src/test/java/fluent/api/EndProcessorTest.java

Maybe my last remark, as the annotations are really used differently by the javac, there are some limitations. E.g. it's not suitable for triggering java code generation, because the compiler doesn't pick files created during this phase for further compilation.

Ondřej Fischer
  • 411
  • 1
  • 7
  • 16
  • 1
    A note for java 8: Make sure the tools.jar is in the classpath since it is in a different location as the other libraries (jdk/lib vs jdk/jre/lib). Although this seems more cumbersome, since the com.sun.source API seems more fine grained than the javax.lang.model API, it provides me with all the tools I need. – Trinova Mar 22 '19 at 12:27
  • Yes, thanks for adding this point. Also note, that since java 9 this changes so, that tools.jar is not needed any more, and the `javax.lang.model` (public) api is available out of the box. On the other hand the internal `com.sun.source` api is not available in java 9 at all. Definitely the later is more flexible, but if you plan to migrate to java 9 or higher, rather stay with the public api. – Ondřej Fischer Mar 22 '19 at 18:09
  • This answer may mislead some to believe you cannot get access to TYPE_USE annotations with an annotation processor. You can but only through the `TypeMirror` (as it is annotated the type) and thus you cannot do a global search and claim for them like normal annotations. **You can however check to see if variable, field, etc has a TYPE_USE annotation on it** through `TypeMirror.getAnnotationMirrors()` – Adam Gent Aug 02 '21 at 20:54