JDK-8306815 : Type argument annotations are not read from byte code by annotation processor
  • Type: Bug
  • Component: tools
  • Sub-Component: apt
  • Affected Version: 17,20,21
  • Priority: P4
  • Status: Resolved
  • Resolution: Duplicate
  • OS: generic
  • CPU: generic
  • Submitted: 2023-04-19
  • Updated: 2023-10-25
  • Resolved: 2023-10-25
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 22
22Resolved
Related Reports
Duplicate :  
Description
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



Comments
The issue is reproducible. When executed with gradle build: PS C:\test\annot-processor-issue> gradle build > Task :module:compileJava C:\test\annot-processor-issue\module\src\main\java\example\module\ExternalClass.java:9: Note: foo:List<*String> public class ExternalClass { ^ > Task :consumer:compileJava C:\test\annot-processor-issue\consumer\src\main\java\TestClass.java:8: Note: bar:List<*String> public class TestClass extends ExternalClass { ^ C:\test\annot-processor-issue\consumer\src\main\java\TestClass.java:8: Note: foo:List<String> public class TestClass extends ExternalClass { ^ BUILD SUCCESSFUL in 2s 10 actionable tasks: 10 executed
25-04-2023