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 9b4176bc49 GROOVY-12160: @AnnotationCollector emits collected
annotation members in hash order when the collector is precompiled
9b4176bc49 is described below
commit 9b4176bc49be4361134d4ed274bcc6a56145488d
Author: Paul King <[email protected]>
AuthorDate: Mon Jul 13 18:26:01 2026 +1000
GROOVY-12160: @AnnotationCollector emits collected annotation members in
hash order when the collector is precompiled
---
.../transform/AnnotationCollectorTransform.java | 8 +-
.../AnnotationCollectorMemberOrderTest.groovy | 97 ++++++++++++++++++++++
2 files changed, 103 insertions(+), 2 deletions(-)
diff --git
a/src/main/java/org/codehaus/groovy/transform/AnnotationCollectorTransform.java
b/src/main/java/org/codehaus/groovy/transform/AnnotationCollectorTransform.java
index 543da80fec..0cc7562db6 100644
---
a/src/main/java/org/codehaus/groovy/transform/AnnotationCollectorTransform.java
+++
b/src/main/java/org/codehaus/groovy/transform/AnnotationCollectorTransform.java
@@ -45,8 +45,8 @@ import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
@@ -328,7 +328,11 @@ public class AnnotationCollectorTransform {
if (member.isEmpty()) {
continue;
}
- Map<String, Expression> generated = new HashMap<>(member.size());
+ // insertion-ordered: copyMembers iterates this map to populate
the annotation's
+ // members, which are written to the class file in that order; the
incoming map
+ // preserves the order the members were declared on the collector,
so hash order
+ // here would emit them in an arbitrary order
+ Map<String, Expression> generated = new
LinkedHashMap<>(member.size());
for (Map.Entry<String, Object> entry : member.entrySet()) {
generated.put(entry.getKey(),
makeExpression(entry.getValue()));
}
diff --git
a/src/test/groovy/org/codehaus/groovy/transform/AnnotationCollectorMemberOrderTest.groovy
b/src/test/groovy/org/codehaus/groovy/transform/AnnotationCollectorMemberOrderTest.groovy
new file mode 100644
index 0000000000..95cd4ba749
--- /dev/null
+++
b/src/test/groovy/org/codehaus/groovy/transform/AnnotationCollectorMemberOrderTest.groovy
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.transform
+
+import org.codehaus.groovy.control.CompilationUnit
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.Phases
+import org.junit.jupiter.api.Test
+import org.objectweb.asm.AnnotationVisitor
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.Opcodes
+
+final class AnnotationCollectorMemberOrderTest {
+
+ /**
+ * When a collector annotation is precompiled, its members are read back
from the
+ * generated CollectorHelper and copied onto the annotations it expands
to. They must
+ * be copied in the order they were declared on the collector, not in hash
order, since
+ * that order is written into the class file's annotation attribute.
+ * <p>
+ * A collector declared in the same compilation unit takes an
order-preserving AST path,
+ * so this only shows up when the collector comes from the classpath.
+ */
+ @Test
+ void testCollectedMembersAreEmittedInDeclarationOrder() {
+ File dir = File.createTempDir()
+ try {
+ // compilation unit 1: the annotation and the collector that
presets its members
+ def config = new CompilerConfiguration(targetDirectory: dir)
+ def cu1 = new CompilationUnit(config)
+ cu1.addSource('Coll.groovy', '''
+ import groovy.transform.AnnotationCollector
+ import java.lang.annotation.*
+
+ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
+ @interface Marker {
+ String alpha() default ''
+ String beta() default ''
+ String gamma() default ''
+ String delta() default ''
+ String epsilon() default ''
+ }
+
+ @Marker(alpha='a', beta='b', gamma='c', delta='d', epsilon='e')
+ @AnnotationCollector
+ @interface Coll {}
+ ''')
+ cu1.compile()
+
+ // compilation unit 2: applies the precompiled collector
+ def loader = new GroovyClassLoader(getClass().classLoader)
+ loader.addClasspath(dir.absolutePath)
+ def config2 = new CompilerConfiguration()
+ config2.setClasspath(dir.absolutePath)
+ def cu2 = new CompilationUnit(config2, null, loader)
+ cu2.addSource('Holder.groovy', '''
+ @Coll
+ class Holder {}
+ ''')
+ cu2.compile(Phases.CLASS_GENERATION)
+
+ byte[] bytes = cu2.classes.find { it.name == 'Holder' }.bytes
+
+ def members = []
+ new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+ @Override
+ AnnotationVisitor visitAnnotation(String descriptor, boolean
visible) {
+ if (descriptor != 'LMarker;') return null
+ return new AnnotationVisitor(Opcodes.ASM9) {
+ @Override void visit(String name, Object value) {
members << name }
+ }
+ }
+ }, 0)
+
+ assert members == ['alpha', 'beta', 'gamma', 'delta', 'epsilon']
+ } finally {
+ dir.deleteDir()
+ }
+ }
+}