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

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

commit bea1ca1ed60a2c113bbf6f414f6a47d2ee4d099f
Author: Paul King <[email protected]>
AuthorDate: Mon Jul 13 18:07:38 2026 +1000

    GROOVY-12159: Synthetic class-literal fields and accessors are emitted in 
hash order, not the order the literals were encountered
---
 .../groovy/classgen/AsmClassGenerator.java         |  6 +-
 .../SyntheticClassLiteralFieldOrderTest.groovy     | 94 ++++++++++++++++++++++
 2 files changed, 99 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java 
b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
index 9592e7840f..5e233297ad 100644
--- a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
+++ b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
@@ -128,6 +128,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -265,7 +266,10 @@ public class AsmClassGenerator extends ClassGenerator {
     private static final MethodCaller createPojoWrapperMethod = 
MethodCaller.newStatic(ScriptBytecodeAdapter.class, "createPojoWrapper");
     private static final MethodCaller createGroovyObjectWrapperMethod = 
MethodCaller.newStatic(ScriptBytecodeAdapter.class, 
"createGroovyObjectWrapper");
 
-    private final Map<String,ClassNode> referencedClasses = new HashMap<>();
+    // insertion-ordered: iterated to generate the synthetic class-literal 
fields and their
+    // accessors, so hash order would place them in an arbitrary order in the 
class file
+    // rather than the order the class literals were encountered
+    private final Map<String, ClassNode> referencedClasses = new 
LinkedHashMap<>();
 
     /**
      * Add marker in the bytecode to show source-bytecode relationship.
diff --git 
a/src/test/groovy/org/codehaus/groovy/classgen/SyntheticClassLiteralFieldOrderTest.groovy
 
b/src/test/groovy/org/codehaus/groovy/classgen/SyntheticClassLiteralFieldOrderTest.groovy
new file mode 100644
index 0000000000..9c09512191
--- /dev/null
+++ 
b/src/test/groovy/org/codehaus/groovy/classgen/SyntheticClassLiteralFieldOrderTest.groovy
@@ -0,0 +1,94 @@
+/*
+ *  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.classgen
+
+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.ClassReader
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.FieldVisitor
+import org.objectweb.asm.MethodVisitor
+import org.objectweb.asm.Opcodes
+
+final class SyntheticClassLiteralFieldOrderTest {
+
+    /**
+     * A class literal naming a non-public type from another compilation unit 
cannot be
+     * emitted as an ldc (see BytecodeHelper#isClassLiteralPossible), so 
AsmClassGenerator
+     * emits a synthetic {@code $class$...} field plus a {@code 
$get$$class$...} accessor
+     * for it. Those must be emitted in the order the literals were 
encountered, not in the
+     * hash order of the map collecting them, so that identical sources 
produce identical
+     * class files.
+     */
+    @Test
+    void testSyntheticClassLiteralMembersAreEmittedInEncounterOrder() {
+        File dir = File.createTempDir()
+        try {
+            // compilation unit 1: the non-public types
+            def config = new CompilerConfiguration(targetDirectory: dir)
+            def cu1 = new CompilationUnit(config)
+            cu1.addSource('Hidden.groovy', '''
+                package p
+                import groovy.transform.PackageScope
+                @PackageScope class Ha {}
+                @PackageScope class Hb {}
+                @PackageScope class Hc {}
+                @PackageScope class Hd {}
+                @PackageScope class He {}
+            ''')
+            cu1.compile()
+
+            // compilation unit 2: references their class literals, in order
+            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('User.groovy', '''
+                class User {
+                    def f() { [p.Ha, p.Hb, p.Hc, p.Hd, p.He] }
+                }
+            ''')
+            cu2.compile(Phases.CLASS_GENERATION)
+
+            byte[] bytes = cu2.classes.find { it.name == 'User' }.bytes
+
+            def fields = [], accessors = []
+            new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+                @Override
+                FieldVisitor visitField(int access, String name, String 
descriptor, String signature, Object value) {
+                    if (name.startsWith('$class$')) fields << name - 
'$class$p$'
+                    return null
+                }
+                @Override
+                MethodVisitor visitMethod(int access, String name, String 
descriptor, String signature, String[] exceptions) {
+                    if (name.startsWith('$get$$class$')) accessors << name - 
'$get$$class$p$'
+                    return null
+                }
+            }, 0)
+
+            assert fields == ['Ha', 'Hb', 'Hc', 'Hd', 'He']
+            assert accessors == ['Ha', 'Hb', 'Hc', 'Hd', 'He']
+        } finally {
+            dir.deleteDir()
+        }
+    }
+}

Reply via email to