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, '''