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 310527e8cc GROOVY-12159: Synthetic class-literal fields and accessors
are emitted in hash order, not the order the literals were encountered
310527e8cc is described below
commit 310527e8cc5fb99b807aac561fb83272a175e0c2
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 6f92fece54..46bea1fb5d 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;
@@ -281,7 +282,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()
+ }
+ }
+}