Get value as Class<?> for an Annotation using the jdk.compiler module

162 Views Asked by At

I'm writing a Java compiler plugin using the jdk.compiler module and Java 15 with preview features enabled. The jdk.compiler module, introduced in Java 9, according to Oracle, contains all the APIs we used to have in java 8's tools.jar except for the com.sun.tools.javac.tree package. What I want to do is find all the classes that are annotated with my annotation and get the value() attribute, in this case of type Class<?>.

Plugin's code:

public class ExtensionMethodPlugin implements Plugin{
  @Override
  public String getName() {
    return "ExtensionMethodPlugin";
  }

  @Override
  public void init(JavacTask task, String... args) {
    task.addTaskListener(new TaskListener() {
      @Override
      public void started(TaskEvent event) {

      }

      @Override
      public void finished(TaskEvent event) {
        if (event.getKind() != TaskEvent.Kind.PARSE) {
          return;
        }

        event.getCompilationUnit().accept(new TreeScanner<Void, Void>() {
          @Override
          public Void visitClass(ClassTree node, Void aVoid) {
            var trees = Trees.instance(task);
            var targetClass = InstrumentationUtils.findExtensionTargetForClass(node, trees, event.getCompilationUnit());
            if (targetClass.isEmpty()) {
              return super.visitClass(node, aVoid);
            }

            var methods = node
              .getMembers()
              .stream()
              .filter(e -> e instanceof MethodTree)
              .map(e -> (MethodTree) e)
              .filter(tree -> tree.getReturnType() != null)
              .filter(e -> InstrumentationUtils.shouldInstrumentMethod(e, targetClass.get()))
              .collect(Collectors.toList());

            throw new RuntimeException("Not implemented");
          }
        }, null);
      }
    });
  }
}

InstrumentationUtils' code:

@UtilityClass
public class InstrumentationUtils {
  public Optional<Class<?>> findExtensionTargetForClass(ClassTree node, Trees trees, CompilationUnitTree tree) {
    return node
      .getModifiers()
      .getAnnotations()
      .stream()
      .filter(a -> Extension.class.getSimpleName().equals(a.getAnnotationType().toString()))
      .findFirst()
      .map(e -> InstrumentationUtils.findConstantTargetClassInAnnotationTree(e, trees, tree));
  }

  private Class<?> findConstantTargetClassInAnnotationTree(AnnotationTree tree, Trees trees, CompilationUnitTree compilationUnitTree) {
    return tree
      .getArguments()
      .stream()
      .filter(entry -> entry.getKind() == Tree.Kind.ASSIGNMENT)
      .findFirst()
      .map(e -> (AssignmentTree) e)
      .map(e -> classForName(e, trees, compilationUnitTree))
      .orElseThrow(() -> new RuntimeException("Cannot compile: Missing constant class target in annotation!"));
  }

  private Class<?> classForName(AssignmentTree tree, Trees trees, CompilationUnitTree compilationUnitTree){
    //Don't know how to move from here
  }
}

Annnotation's code:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Extension {
  Class<?> value();
}

The only solutions I've found involve using the JCTree classes, but these, as I said previously, are no longer available. I do know I could force the export of that package in the jdk.compiler module quite easily, but I would prefer to find a better way. I've tried to use a TreeScanner, but this leads nowhere:

 tree.accept(new TreeScanner<Class<?>, Void>() {
      // b.class
      @Override
      public Class<?> visitAssignment(AssignmentTree node, Void unused) {
        return node.getExpression().accept(new TreeScanner<Class<?>, Void>(){
          //.class
          @Override
          public Class<?> visitMemberSelect(MemberSelectTree node, Void unused) {
            return node.getExpression().accept(new TreeScanner<Class<?>, Void>(){
              // class ???
              @Override
              public Class<?> visitIdentifier(IdentifierTree node, Void unused) {
                trees.printMessage(Diagnostic.Kind.WARNING, node.getName(),  tree, compilationUnitTree);
                return super.visitIdentifier(node, unused);
              }
            }, null);
          }
        }, null);
      }
    }, null);

Thanks in advance!

1

There are 1 best solutions below

0
On BEST ANSWER

When all you need to do, is to “find all the classes that are annotated with my annotation and get the value() attribute”, then you don’t need the tree API from the jdk.compiler module at all, the annotation processor API contained in the java.compiler module is sufficient and much simpler to handle.

Here is a self-contained example:

static final String EXAMPLE_CODE =
    "import java.lang.annotation.*;\n" +
    "\n" +
    "@Target({ElementType.TYPE})\n" +
    "@Retention(RetentionPolicy.SOURCE)\n" +
    "@interface Extension {\n" +
    "  Class<?> value();\n" +
    "}\n" +
    "\n" +
    "@Extension(Runnable.class)\n" +
    "public class SomeClass {\n" +
    "    public static void aMethod() {\n" +
    "    }\n" +
    "\n" +
    "    @Extension(SomeClass.class)\n" +
    "    class NestedClassToMakeItMoreExciting {\n" +
    "        \n" +
    "    }\n" +
    "}";

public static void main(String[] args) throws IOException {
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

    StandardJavaFileManager fileManager=compiler.getStandardFileManager(null,null,null);
    Path tmp = Files.createTempDirectory("compile-test-");
    fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Set.of(tmp.toFile()));

    Set<JavaFileObject> srcObj = Set.of(new SimpleJavaFileObject(
        URI.create("string:///SomeClass.java"), JavaFileObject.Kind.SOURCE) {
            @Override
            public CharSequence getCharContent(boolean ignoreEncodingErrors) {
                return EXAMPLE_CODE;
            }
        });

    JavaCompiler.CompilationTask task
        = compiler.getTask(null, fileManager, null, null, null, srcObj);
    task.setProcessors(Set.of(new MyProcessor()));

    task.call();
}

@SupportedAnnotationTypes("Extension")
@SupportedSourceVersion(SourceVersion.RELEASE_14)
static class MyProcessor extends AbstractProcessor {
    @Override
    public boolean process(
        Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        TypeElement ext = processingEnv.getElementUtils().getTypeElement("Extension");

        for(Element annotatedElement: roundEnv.getElementsAnnotatedWith(ext)) {
            System.out.println(annotatedElement);

            for(AnnotationMirror am: annotatedElement.getAnnotationMirrors()) {
                if(am.getAnnotationType().asElement() == ext) {
                    am.getElementValues().forEach((e,v) ->
                        System.out.println("\t"+e.getSimpleName()+" = "+v.getValue()));
                }
            }
        }
        return true;
    }
}

This prints

SomeClass
    value = java.lang.Runnable
SomeClass.NestedClassToMakeItMoreExciting
    value = SomeClass

Of course, this functionality can be combined with features from the jdk.compiler extension when needed.