Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to access TypeUse annotation via AnnotationProcessor

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.

like image 371
Trinova Avatar asked Mar 18 '19 09:03

Trinova


1 Answers

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.

like image 55
Ondřej Fischer Avatar answered Oct 25 '22 11:10

Ondřej Fischer