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
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
- duplicates
-
JDK-8225377 type annotations are not visible to javac plugins across compilation boundaries
- Resolved