Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8306815

Type argument annotations are not read from byte code by annotation processor

XMLWordPrintable

    • apt
    • generic
    • generic

      A DESCRIPTION OF THE PROBLEM :
      Consider we have an annotation with target = TYPE_USE and retention = RUNTIME. Applying this annotation to a type argument makes it appear in bytecode. This annotation is also visible to annotation processor, when annotation processor parses symbol from source code. However, when annotation processor takes symbol from bytecode, it fails to parse corresponding annotation.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      1. Create annotation 'A' with target = TYPE_USE and retention = RUNTIME
      2. Define a class (B) with method that returns something with annotation mentioned in type arguments, for example List<@A String>
      3. Define another class (C) in another module that somehow references the class from the former step, for example, extends it.
      4. Write annotation processor that walks classes transitively, scans their methods and tries to extract annotation 'A'.
      5. Apply annotation processor to C.java with classpath containing B.class


      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      Annotation processor does not see occurrences of annotation 'A'
      ACTUAL -
      Annotation processor should see occurrences of annotation 'A'

      ---------- BEGIN SOURCE ----------
      This issue is not possible to reproduce on a single source file. I did not find a way to attach an example, so I just published it here: https://teavm.org/tmp/annot-processor-issue.zip

      Example produces three compiler messages, namely foo:List<*String>, bar:List<*String> and foo:List<String>. The latter is expected to contain '*' character, i.e. be equal to the first one.

      --- ClassToProcess.java

      package example;

      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;

      @Retention(RetentionPolicy.CLASS)
      @Target(ElementType.TYPE)
      public @interface ClassToProcess {
      }

      --- ExampleAnnot.java

      package example;

      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;

      @Retention(RetentionPolicy.CLASS)
      @Target(ElementType.TYPE_USE)
      public @interface ExampleAnnot {
      }

      --- AnnotationNames.java

      package example.processor;

      final class AnnotationNames {
          static final String CLASS_TO_PROCESS = "example.ClassToProcess";
          static final String EXAMPLE_ANNOT = "example.ExampleAnnot";
      }

      --- ExampleProcessor.java

      package example.processor;

      import javax.annotation.processing.*;
      import javax.lang.model.SourceVersion;
      import javax.lang.model.element.ElementKind;
      import javax.lang.model.element.ExecutableElement;
      import javax.lang.model.element.TypeElement;
      import javax.lang.model.type.DeclaredType;
      import javax.lang.model.type.TypeKind;
      import javax.lang.model.type.TypeMirror;
      import javax.tools.Diagnostic;
      import java.util.Set;
      import java.util.stream.Collectors;

      import static example.processor.AnnotationNames.CLASS_TO_PROCESS;
      import static example.processor.AnnotationNames.EXAMPLE_ANNOT;

      @SupportedSourceVersion(SourceVersion.RELEASE_17)
      @SupportedAnnotationTypes({ CLASS_TO_PROCESS })
      public class ExampleProcessor extends AbstractProcessor {
          private TypeElement exampleAnnot;

          @Override
          public synchronized void init(ProcessingEnvironment processingEnv) {
              super.init(processingEnv);
              exampleAnnot = processingEnv.getElementUtils().getTypeElement(EXAMPLE_ANNOT);
          }

          @Override
          public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
              for (var annot : annotations) {
                  for (var elem : roundEnv.getElementsAnnotatedWith(annot)) {
                      if (elem instanceof TypeElement cls) {
                          scanClass(cls);
                      }
                  }
              }
              return true;
          }

          private void scanClass(TypeElement cls) {
              var originalCls = cls;
              while (true) {
                  for (var member : cls.getEnclosedElements()) {
                      if (member instanceof ExecutableElement executable) {
                          if (executable.getKind() != ElementKind.CONSTRUCTOR) {
                              processingEnv.getMessager().printMessage(
                                      Diagnostic.Kind.NOTE,
                                      executable.getSimpleName() + ":" + printType(executable.getReturnType()),
                                      originalCls
                              );
                          }
                      }
                  }
                  var superclassType = cls.getSuperclass();
                  if (superclassType.getKind() == TypeKind.NONE) {
                      break;
                  }
                  cls = (TypeElement) ((DeclaredType) superclassType).asElement();
                  if (cls.getSuperclass().getKind() == TypeKind.NONE) {
                      break;
                  }
              }
          }

          private String printType(TypeMirror type) {
              var result = printTypeWithoutAnnot(type);
              var hasAnnot = type.getAnnotationMirrors().stream()
                      .anyMatch(am -> am.getAnnotationType().asElement() == exampleAnnot);
              return hasAnnot ? "*" + result : result;
          }

          private String printTypeWithoutAnnot(TypeMirror type) {
              if (type instanceof DeclaredType classType) {
                  var cls = (TypeElement) classType.asElement();
                  var sb = new StringBuilder(cls.getSimpleName().toString());
                  if (!classType.getTypeArguments().isEmpty()) {
                      sb.append("<");
                      sb.append(classType.getTypeArguments().stream().map(this::printType).collect(Collectors.joining(",")));
                      sb.append(">");
                  }
                  return sb.toString();
              } else {
                  return "unsupported";
              }
          }
      }

      --- ExternalClass.java

      package example.module;

      import example.ClassToProcess;
      import example.ExampleAnnot;

      import java.util.List;

      @ClassToProcess
      public class ExternalClass {
          public native List<@ExampleAnnot String> foo();
      }

      --- TestClass.java

      import example.ClassToProcess;
      import example.ExampleAnnot;
      import example.module.ExternalClass;

      import java.util.List;

      @ClassToProcess
      public class TestClass extends ExternalClass {
          public native List<@ExampleAnnot String> bar();
      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      I created another annotation processor that walks types, collects annotations from types and writes them into json files near class file. Then, main annotation processor parses these json files to resolve annotations. However, when I switched build from IDEA JPS to Gradle, it turned out that this approach does not work anymore, since Gradle incremental compiler passes to APT symbols, parsed from class files, which breaks this workaround. So effectively there's no workaround available for my case anymore.

      FREQUENCY : always


            vromero Vicente Arturo Romero Zaldivar
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated:
              Resolved: