From ca07c5ffd3ee20711e518ca9e0ff3749edf1481c Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Thu, 6 Feb 2025 13:54:11 +0100 Subject: [PATCH] fix losing type annotations with different visibility In case a single type annotation target (such as a `MethodInfo`) has multiple type annotations, some of them runtime-retained and some class-retained, all annotations of one kind are lost. This is because `Indexer.processTypeAnnotations()` was written with the assumption that it is only called once for any given target. This was true in Jandex 2, which only supported runtime-retained annotations, but since Jandex 3.0, class-retained annotations are supported as well. Hence, the method can legally be called twice for any given target. Fortunately, this issue doesn't exist for non-type annotations. --- .../main/java/org/jboss/jandex/Indexer.java | 8 ++- ...AndRuntimeTypeAnnotationsOnMethodTest.java | 54 +++++++++++++++++++ .../org/jboss/jandex/test/RecordTestCase.java | 5 ++ .../src/main/java/test/MyClassAnnotation.java | 14 +++++ ...cordWithDifferentVisibilityAnnotation.java | 4 ++ 5 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 core/src/test/java/org/jboss/jandex/test/ClassAndRuntimeTypeAnnotationsOnMethodTest.java create mode 100644 test-data/src/main/java/test/MyClassAnnotation.java create mode 100644 test-data/src/main/java/test/RecordWithDifferentVisibilityAnnotation.java diff --git a/core/src/main/java/org/jboss/jandex/Indexer.java b/core/src/main/java/org/jboss/jandex/Indexer.java index 6c403dfb..90b7482b 100644 --- a/core/src/main/java/org/jboss/jandex/Indexer.java +++ b/core/src/main/java/org/jboss/jandex/Indexer.java @@ -886,7 +886,7 @@ private void processEnclosingMethod(DataInputStream data, ClassInfo target) thro private void processTypeAnnotations(DataInputStream data, AnnotationTarget target, boolean visible) throws IOException { int numAnnotations = data.readUnsignedShort(); - List annotations = new ArrayList(numAnnotations); + List annotations = new ArrayList<>(numAnnotations); for (int i = 0; i < numAnnotations; i++) { TypeAnnotationState annotation = processTypeAnnotation(data, target, visible); @@ -895,7 +895,11 @@ private void processTypeAnnotations(DataInputStream data, AnnotationTarget targe } } - typeAnnotations.put(target, annotations); + if (typeAnnotations.containsKey(target)) { + typeAnnotations.get(target).addAll(annotations); + } else { + typeAnnotations.put(target, annotations); + } } private TypeAnnotationState processTypeAnnotation(DataInputStream data, AnnotationTarget target, boolean visible) diff --git a/core/src/test/java/org/jboss/jandex/test/ClassAndRuntimeTypeAnnotationsOnMethodTest.java b/core/src/test/java/org/jboss/jandex/test/ClassAndRuntimeTypeAnnotationsOnMethodTest.java new file mode 100644 index 00000000..65ccc6f9 --- /dev/null +++ b/core/src/test/java/org/jboss/jandex/test/ClassAndRuntimeTypeAnnotationsOnMethodTest.java @@ -0,0 +1,54 @@ +package org.jboss.jandex.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.PrimitiveType; +import org.jboss.jandex.Type; +import org.junit.jupiter.api.Test; + +public class ClassAndRuntimeTypeAnnotationsOnMethodTest { + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE_USE) + @interface ClassAnnotation { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE_USE) + @interface RuntimeAnnotation { + } + + static class TestMethod { + static void method(@RuntimeAnnotation int foo, @ClassAnnotation String bar) { + } + } + + @Test + public void test() throws IOException { + Index index = Index.of(ClassAnnotation.class, RuntimeAnnotation.class, TestMethod.class); + MethodInfo method = index.getClassByName(TestMethod.class).firstMethod("method"); + assertNotNull(method); + assertEquals(2, method.parametersCount()); + + Type param0 = method.parameterType(0); + assertEquals(Type.Kind.PRIMITIVE, param0.kind()); + assertEquals(PrimitiveType.Primitive.INT, param0.asPrimitiveType().primitive()); + assertEquals(1, param0.annotations().size()); + assertEquals(RuntimeAnnotation.class.getName(), param0.annotations().get(0).name().toString()); + + Type param1 = method.parameterType(1); + assertEquals(Type.Kind.CLASS, param1.kind()); + assertEquals(DotName.STRING_NAME, param1.asClassType().name()); + assertEquals(1, param1.annotations().size()); + assertEquals(ClassAnnotation.class.getName(), param1.annotations().get(0).name().toString()); + } +} diff --git a/core/src/test/java/org/jboss/jandex/test/RecordTestCase.java b/core/src/test/java/org/jboss/jandex/test/RecordTestCase.java index 94c5fd6d..2d283dd8 100644 --- a/core/src/test/java/org/jboss/jandex/test/RecordTestCase.java +++ b/core/src/test/java/org/jboss/jandex/test/RecordTestCase.java @@ -196,6 +196,10 @@ public void canonicalCtor() { assertEquals(1, rec.constructors().size()); assertEquals(rec.constructors().get(0), rec.canonicalRecordConstructor()); + rec = index.getClassByName("test.RecordWithDifferentVisibilityAnnotation"); + assertEquals(1, rec.constructors().size()); + assertEquals(rec.constructors().get(0), rec.canonicalRecordConstructor()); + rec = index.getClassByName("test.RecordWithMultipleCtorsAndDefaultCanonicalCtor"); assertEquals(4, rec.constructors().size()); assertEquals(2, rec.canonicalRecordConstructor().parametersCount()); @@ -235,6 +239,7 @@ private Index buildIndex() throws IOException { indexer.index(getClass().getClassLoader().getResourceAsStream("test/Record2WithCustomCanonicalCtor.class")); indexer.index(getClass().getClassLoader().getResourceAsStream("test/Record2WithDefaultCanonicalCtor.class")); indexer.index(getClass().getClassLoader().getResourceAsStream("test/RecordWithBuggyAnnotation.class")); + indexer.index(getClass().getClassLoader().getResourceAsStream("test/RecordWithDifferentVisibilityAnnotation.class")); indexer.index( getClass().getClassLoader().getResourceAsStream("test/RecordWithMultipleCtorsAndCompactCanonicalCtor.class")); indexer.index( diff --git a/test-data/src/main/java/test/MyClassAnnotation.java b/test-data/src/main/java/test/MyClassAnnotation.java new file mode 100644 index 00000000..13dcefb0 --- /dev/null +++ b/test-data/src/main/java/test/MyClassAnnotation.java @@ -0,0 +1,14 @@ +package test; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// copy of `MyAnnotation`, just different retention +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, + ElementType.TYPE_USE }) +@interface MyClassAnnotation { + String value(); +} diff --git a/test-data/src/main/java/test/RecordWithDifferentVisibilityAnnotation.java b/test-data/src/main/java/test/RecordWithDifferentVisibilityAnnotation.java new file mode 100644 index 00000000..04f724bb --- /dev/null +++ b/test-data/src/main/java/test/RecordWithDifferentVisibilityAnnotation.java @@ -0,0 +1,4 @@ +package test; + +public record RecordWithDifferentVisibilityAnnotation(@MyAnnotation("foo") int foo, @MyClassAnnotation("bar") String bar) { +}