You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/10/13 09:31:16 UTC

(groovy) branch master updated: GROOVY-12434: Implementing a precompiled trait fails with "Duplicate annotation" when a trait method has a TYPE_USE annotation on its return type

This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new d163cd0ceb GROOVY-12434: Implementing a precompiled trait fails with "Duplicate annotation" when a trait method has a TYPE_USE annotation on its return type
d163cd0ceb is described below

commit d163cd0ceb9cc4fddb283fad8a7099d646d04f46
Author: Paul King <pa...@asert.com.au>
AuthorDate: Fri Oct 9 02:38:17 2026 +1000

    GROOVY-12434: Implementing a precompiled trait fails with "Duplicate annotation" when a trait method has a TYPE_USE annotation on its return type
---
 .../codehaus/groovy/classgen/ExtendedVerifier.java | 25 +++++++++-
 .../org/codehaus/groovy/vmplugin/v16/Java16.java   |  2 +-
 .../org/codehaus/groovy/vmplugin/v8/Java8.java     | 54 +++++++++++++++++-----
 .../ReflectionTypeAnnotationsTest.groovy           | 32 +++++++++++++
 .../traitx/TraitASTTransformationTest.groovy       | 26 +++++++++++
 5 files changed, 125 insertions(+), 14 deletions(-)

diff --git a/src/main/java/org/codehaus/groovy/classgen/ExtendedVerifier.java b/src/main/java/org/codehaus/groovy/classgen/ExtendedVerifier.java
index d64edecca7..d5826fa499 100644
--- a/src/main/java/org/codehaus/groovy/classgen/ExtendedVerifier.java
+++ b/src/main/java/org/codehaus/groovy/classgen/ExtendedVerifier.java
@@ -339,7 +339,14 @@ public class ExtendedVerifier extends ClassCodeVisitorSupport {
         }
         if (!typeUseAnnos.isEmpty()) {
             while (targetType.isArray()) targetType = targetType.getComponentType();
-            targetType.addTypeAnnotations(typeUseAnnos);
+            // GROOVY-12434: a type node shared with a copied member (e.g. a trait method and
+            // its helper or forwarder) may already hold the same annotation or a copy of it
+            List<AnnotationNode> existing = targetType.getTypeAnnotations();
+            for (AnnotationNode anno : typeUseAnnos) {
+                if (existing.stream().noneMatch(e -> isSameAnnotation(e, anno))) {
+                    targetType.addTypeAnnotation(anno);
+                }
+            }
             for (AnnotationNode anno : typeUseAnnos) {
                 if (!anno.isTargetAllowed(keepTarget)) {
                     mixed.remove(anno);
@@ -348,6 +355,22 @@ public class ExtendedVerifier extends ClassCodeVisitorSupport {
         }
     }
 
+    /**
+     * Duplicates are already rejected or collected into a container for each member, so an
+     * annotation of the same type with the same member values is the same annotation or a copy
+     * of it, as made by {@code GeneralUtils#copyAnnotatedNodeAnnotations}.
+     */
+    private static boolean isSameAnnotation(final AnnotationNode a, final AnnotationNode b) {
+        if (a == b) return true;
+        if (!a.getClassNode().equals(b.getClassNode())) return false;
+        Map<String, Expression> aMembers = a.getMembers(), bMembers = b.getMembers();
+        if (aMembers.size() != bMembers.size()) return false;
+        for (Map.Entry<String, Expression> member : aMembers.entrySet()) {
+            if (bMembers.get(member.getKey()) != member.getValue()) return false;
+        }
+        return true;
+    }
+
     /**
      * Visits the annotations attached to the supplied node for the given target kind.
      *
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v16/Java16.java b/src/main/java/org/codehaus/groovy/vmplugin/v16/Java16.java
index dec1bc53df..f2e1a12b4e 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v16/Java16.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v16/Java16.java
@@ -105,7 +105,7 @@ public class Java16 extends Java10 {
     protected void makeRecordComponents(final CompileUnit cu, final ClassNode cn, final Class<?> c) {
         if (c.isRecord()) cn.setRecordComponents(Arrays.stream(c.getRecordComponents()).map(rc -> {
             ClassNode type = makeClassNode(cu, rc.getGenericType(), rc.getType());
-            type = applyTypeAnnotations(rc.getAnnotatedType(), type);
+            type = applyTypeAnnotations(rc::getAnnotatedType, type);
             return new RecordComponentNode(cn, rc.getName(), type, Arrays.stream(rc.getAnnotations()).map(this::toAnnotationNode).collect(Collectors.toList()));
         }).collect(Collectors.toList()));
     }
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java
index 089f021bd3..36710b9897 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java
@@ -46,6 +46,7 @@ import org.codehaus.groovy.vmplugin.VMPluginFactory;
 import org.codehaus.groovy.vmplugin.v17.Java17;
 
 import java.lang.annotation.Annotation;
+import java.lang.annotation.AnnotationFormatError;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -74,6 +75,7 @@ import java.lang.reflect.TypeVariable;
 import java.lang.reflect.WildcardType;
 import java.util.Arrays;
 import java.util.List;
+import java.util.function.Supplier;
 
 /**
  * Java 8 based functions.
@@ -260,6 +262,7 @@ public class Java8 implements VMPlugin {
      */
     private static final boolean TYPE_ANNOTATIONS = typeAnnotationsAvailable();
     private static final Annotation[] NO_ANNOTATIONS = new Annotation[0];
+    private static final AnnotatedType[] NO_ANNOTATED_TYPES = new AnnotatedType[0];
 
     static boolean typeAnnotationsAvailable() {
         try {
@@ -381,7 +384,7 @@ public class Java8 implements VMPlugin {
             Field[] fields = clazz.getDeclaredFields();
             for (Field f : fields) {
                 ClassNode rt = makeClassNode(compileUnit, f.getGenericType(), f.getType());
-                if (TYPE_ANNOTATIONS) rt = applyTypeAnnotations(f.getAnnotatedType(), rt);
+                if (TYPE_ANNOTATIONS) rt = applyTypeAnnotations(f::getAnnotatedType, rt);
                 FieldNode fn = new FieldNode(f.getName(), f.getModifiers(), rt, classNode, getValue(f));
                 setAnnotationMetaData(f.getAnnotations(), fn);
                 classNode.addField(fn);
@@ -389,7 +392,7 @@ public class Java8 implements VMPlugin {
             Method[] methods = ReflectionUtils.getDeclaredMethodsSorted(clazz);
             for (Method m : methods) {
                 ClassNode rt = makeClassNode(compileUnit, m.getGenericReturnType(), m.getReturnType());
-                if (TYPE_ANNOTATIONS) rt = applyTypeAnnotations(m.getAnnotatedReturnType(), rt);
+                if (TYPE_ANNOTATIONS) rt = applyTypeAnnotations(m::getAnnotatedReturnType, rt);
                 Parameter[] params = makeParameters(compileUnit, m.getGenericParameterTypes(), m.getParameterTypes(), m.getParameterAnnotations(), m);
                 ClassNode[] exceptions = makeClassNodes(compileUnit, m.getGenericExceptionTypes(), m.getExceptionTypes());
                 applyExceptionTypeAnnotations(m, exceptions);
@@ -419,10 +422,7 @@ public class Java8 implements VMPlugin {
             Class<?> sc = clazz.getSuperclass();
             if (sc != null) {
                 ClassNode superClass = makeClassNode(compileUnit, clazz.getGenericSuperclass(), sc);
-                if (TYPE_ANNOTATIONS) {
-                    AnnotatedType annotatedSuperclass = clazz.getAnnotatedSuperclass();
-                    if (annotatedSuperclass != null) superClass = applyTypeAnnotations(annotatedSuperclass, superClass);
-                }
+                if (TYPE_ANNOTATIONS) superClass = applyTypeAnnotations(clazz::getAnnotatedSuperclass, superClass);
                 classNode.setUnresolvedSuperClass(superClass);
             }
             makeInterfaceTypes(compileUnit, classNode, clazz);
@@ -547,9 +547,10 @@ public class Java8 implements VMPlugin {
                 ret[i] = makeClassNode(cu, interfaceTypes[i], (Class<?>) type);
             }
             if (TYPE_ANNOTATIONS) {
-                AnnotatedType[] annotatedInterfaces = clazz.getAnnotatedInterfaces();
+                AnnotatedType[] annotatedInterfaces = getAnnotatedTypes(clazz::getAnnotatedInterfaces);
                 for (int i = 0, m = Math.min(annotatedInterfaces.length, n); i < m; i += 1) {
-                    ret[i] = applyTypeAnnotations(annotatedInterfaces[i], ret[i]);
+                    AnnotatedType annotatedInterface = annotatedInterfaces[i];
+                    ret[i] = applyTypeAnnotations(() -> annotatedInterface, ret[i]);
                 }
             }
             classNode.setInterfaces(ret);
@@ -585,6 +586,33 @@ public class Java8 implements VMPlugin {
         return back.getPlainNodeReference();
     }
 
+    /**
+     * Applies type-use annotations (JSR 308) of the supplied annotated type to the given class node,
+     * leaving the class node unchanged if the class file holds type annotations the JDK rejects;
+     * for example, a non-repeatable annotation written twice by an earlier Groovy version.
+     *
+     * @param annotatedType supplies the reflective annotated type, or {@code null} if there is none
+     * @param classNode the class node created for that position
+     * @return the annotated class node: either the input node or a per-use replacement for it
+     * @since 6.0.1
+     */
+    protected ClassNode applyTypeAnnotations(final Supplier<AnnotatedType> annotatedType, final ClassNode classNode) {
+        try {
+            AnnotatedType type = annotatedType.get();
+            return type != null ? applyTypeAnnotations(type, classNode) : classNode;
+        } catch (AnnotationFormatError e) { // GROOVY-12434
+            return classNode;
+        }
+    }
+
+    private static AnnotatedType[] getAnnotatedTypes(final Supplier<AnnotatedType[]> annotatedTypes) {
+        try {
+            return annotatedTypes.get();
+        } catch (AnnotationFormatError e) { // GROOVY-12434
+            return NO_ANNOTATED_TYPES;
+        }
+    }
+
     /**
      * Applies type-use annotations (JSR 308) of the given annotated type to the given class node,
      * recursing into type arguments, wildcard bounds and array component types. The returned node
@@ -673,10 +701,11 @@ public class Java8 implements VMPlugin {
             }
             // synthetic parameters (e.g. of inner class constructors) may not be
             // included in the annotated parameter types; skip on length mismatch
-            AnnotatedType[] annotatedTypes = TYPE_ANNOTATIONS ? ((Executable) member).getAnnotatedParameterTypes() : null;
+            AnnotatedType[] annotatedTypes = TYPE_ANNOTATIONS ? getAnnotatedTypes(((Executable) member)::getAnnotatedParameterTypes) : null;
             if (annotatedTypes != null && annotatedTypes.length == n) {
                 for (int i = 0; i < n; i += 1) {
-                    params[i].setType(applyTypeAnnotations(annotatedTypes[i], params[i].getType()));
+                    AnnotatedType annotatedType = annotatedTypes[i];
+                    params[i].setType(applyTypeAnnotations(() -> annotatedType, params[i].getType()));
                 }
             }
         }
@@ -692,9 +721,10 @@ public class Java8 implements VMPlugin {
      */
     private void applyExceptionTypeAnnotations(final Executable member, final ClassNode[] exceptions) {
         if (!TYPE_ANNOTATIONS) return;
-        AnnotatedType[] annotatedTypes = member.getAnnotatedExceptionTypes();
+        AnnotatedType[] annotatedTypes = getAnnotatedTypes(member::getAnnotatedExceptionTypes);
         for (int i = 0, n = Math.min(annotatedTypes.length, exceptions.length); i < n; i += 1) {
-            exceptions[i] = applyTypeAnnotations(annotatedTypes[i], exceptions[i]);
+            AnnotatedType annotatedType = annotatedTypes[i];
+            exceptions[i] = applyTypeAnnotations(() -> annotatedType, exceptions[i]);
         }
     }
 
diff --git a/src/test/groovy/org/codehaus/groovy/ast/decompiled/ReflectionTypeAnnotationsTest.groovy b/src/test/groovy/org/codehaus/groovy/ast/decompiled/ReflectionTypeAnnotationsTest.groovy
index b7eb64697e..14c70ee75e 100644
--- a/src/test/groovy/org/codehaus/groovy/ast/decompiled/ReflectionTypeAnnotationsTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/ast/decompiled/ReflectionTypeAnnotationsTest.groovy
@@ -20,8 +20,14 @@ package org.codehaus.groovy.ast.decompiled
 
 import org.codehaus.groovy.ast.ClassHelper
 import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.ast.decompiled.support.WhereAnno
 import org.codehaus.groovy.ast.expr.ConstantExpression
 import org.junit.jupiter.api.Test
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.MethodVisitor
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.Type
+import org.objectweb.asm.TypeReference
 
 /**
  * Tests that the reflection path ({@code VMPlugin#configureClassNode}) ingests type-use
@@ -98,6 +104,32 @@ final class ReflectionTypeAnnotationsTest {
         assert ClassHelper.int_TYPE.typeAnnotations.isEmpty()
     }
 
+    // GROOVY-12434: class files from earlier Groovy versions may repeat a non-repeatable type annotation
+    @Test
+    void "duplicate type use annotations are skipped"() {
+        def writer = new ClassWriter(0)
+        writer.visit(Opcodes.V17, Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT, 'DuplicateTypeAnnotations', null, 'java/lang/Object', null)
+        def returnAnno = { MethodVisitor mv, String value ->
+            def av = mv.visitTypeAnnotation(TypeReference.newTypeReference(TypeReference.METHOD_RETURN).value, null, Type.getDescriptor(WhereAnno), true)
+            av.visit('value', value)
+            av.visitEnd()
+        }
+        def mv = writer.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT, 'dup', '()Ljava/lang/String;', null, null)
+        returnAnno(mv, 'dup')
+        returnAnno(mv, 'dup')
+        mv.visitEnd()
+        mv = writer.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT, 'ok', '()Ljava/lang/String;', null, null)
+        returnAnno(mv, 'ok')
+        mv.visitEnd()
+        writer.visitEnd()
+
+        def loader = new GroovyClassLoader(getClass().classLoader)
+        def type = ClassHelper.make(loader.defineClass('DuplicateTypeAnnotations', writer.toByteArray()))
+
+        assert annos(type.getDeclaredMethods('dup')[0].returnType) == []
+        assert annos(type.getDeclaredMethods('ok' )[0].returnType) == ['ok']
+    }
+
     private static List<String> annos(ClassNode type) {
         type.typeAnnotations.collect { ((ConstantExpression) it.getMember('value')).text }
     }
diff --git a/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitASTTransformationTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitASTTransformationTest.groovy
index 34b57d02da..dfefb2e020 100644
--- a/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitASTTransformationTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitASTTransformationTest.groovy
@@ -2164,6 +2164,32 @@ final class TraitASTTransformationTest {
         '''
     }
 
+    // GROOVY-12434
+    @Test
+    void testAnnotationShouldBeCarriedOver3() {
+        assertScript shell, '''
+            // TYPE_USE annotation defined outside the script, so reflection can resolve it
+            import org.codehaus.groovy.ast.decompiled.support.WhereAnno
+
+            trait Named {
+                @WhereAnno('getName') String getName() { null }
+                static @WhereAnno('make') String make() { null }
+            }
+            class Person implements Named {
+            }
+
+            def helper = Named.classLoader.loadClass('Named$Trait$Helper')
+            for (type in [Named, helper]) {
+                for (name in ['getName', 'make']) {
+                    def method = type.declaredMethods.find { it.name == name }
+                    // reading a duplicated annotation throws AnnotationFormatError
+                    assert method.annotatedReturnType.annotations*.value() == [name]
+                }
+            }
+            assert new Person().name == null
+        '''
+    }
+
     @Test
     void testShouldCompileTraitMethodStatically() {
         def err = shouldFail shell, '''