GROOVY-12149: order reflection calls to ensure Annotation members are ordered#2690
Conversation
|
@paulk-asert I found more issues after verifying the restage. MOP bridge methods & Woven trait methods. I used a similar fix and extracted the logic to a shared location. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## GROOVY_5_0_X #2690 +/- ##
======================================================
- Coverage 67.2505% 67.2484% -0.0021%
- Complexity 29569 29578 +9
======================================================
Files 1382 1382
Lines 116991 117008 +17
Branches 20551 20556 +5
======================================================
+ Hits 78677 78686 +9
- Misses 31787 31794 +7
- Partials 6527 6528 +1
🚀 New features to boost your workflow:
|
|
Claude recommends the following:
|
db4fa0c to
304a046
Compare
|
On the drop return type note: Because that "tie" is exactly the failure. Sorting only by name + parameters means a bridge method and the method it bridges compare equal (return 0). Arrays.sort is stable — TimSort — so equal elements keep their input order. And the input order is getDeclaredMethods(), the very thing that's nondeterministic and varies between JVM runs. So the tie doesn't get resolved; it silently inherits the nondeterminism you're trying to eliminate. Concretely, covariant returns / generic erasure produce this: class Base { T get() { ... } } Sub has two get methods: same name, identical parameter types, differing only in return type (String vs Object). With a name+params comparator they tie, and whichever the JVM happened to enumerate first wins — per build. Adding the return-type key breaks that last tie, so no two distinct methods ever compare equal. That's what "total order" buys: the JVM forbids two methods in one class sharing a name and descriptor, and the descriptor is precisely parameters + return type. Compare on the full descriptor and every element has a unique sort key, so the output is fully determined no matter what order getDeclaredMethods() hands back. Leave out the return type and the ordering is merely partial — total everywhere except the bridge pairs, which are the one place reproducibility actually needed it. (Constructors don't need it: name is always and return type always void, so parameters alone are a total order there — which is why getDeclaredConstructorsSorted stops at compareParameterTypes.) |
|
Should the order match the CachedMethods order or should return be last in the sort is my only question. Currently this implementation is comparing the return last, while CachedMethods compares it in the middle. |
…bytecode, breaking reproducible builds
|
There is an error grabbing grapes, which leads me to believe this was a transient issue. Can someone please rekick off the tests for this PR? |
There was a problem hiding this comment.
Pull request overview
This PR aims to make reflection-derived member enumeration deterministic so that Groovy’s compile-time processing of precompiled classes yields reproducible (byte-identical) generated class files, specifically addressing annotation member ordering and related reflection-driven ClassNode construction.
Changes:
- Added deterministic sorting helpers in
ReflectionUtilsfor declared methods and constructors. - Updated
Java8VM plugin code paths to use the sorted reflection results for annotation members, methods, and constructors. - Removed the inline annotation-member sorting logic in favor of the centralized helper.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java | Uses new ReflectionUtils sorted reflection helpers for annotation member processing and for class method/constructor enumeration. |
| src/main/java/org/codehaus/groovy/reflection/ReflectionUtils.java | Introduces deterministic ordering APIs for getDeclaredMethods() and getDeclaredConstructors() via internal comparison helpers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| setAnnotationMetaData(f.getAnnotations(), fn); | ||
| classNode.addField(fn); | ||
| } | ||
| Method[] methods = clazz.getDeclaredMethods(); | ||
| Method[] methods = ReflectionUtils.getDeclaredMethodsSorted(clazz); | ||
| for (Method m : methods) { |
| /** | ||
| * Returns the declared methods of a class in a deterministic order. | ||
| * <p> | ||
| * {@link Class#getDeclaredMethods()} does not guarantee an order, and | ||
| * HotSpot's varies between JVM runs. When members of a precompiled class | ||
| * are enumerated at compile time (annotation members copied to generated | ||
| * code, trait methods woven into implementing classes, MOP {@code super$} | ||
| * bridge methods, etc.), that order flows into the generated bytecode, so | ||
| * a nondeterministic order produces byte-different class files from | ||
| * identical sources. Callers whose output depends on member order should | ||
| * use this variant so builds are reproducible. | ||
| * | ||
| * @param type the class to introspect | ||
| * @return the declared methods, sorted by name then signature | ||
| * @since 5.0.8 | ||
| */ | ||
| public static Method[] getDeclaredMethodsSorted(final Class<?> type) { | ||
| Method[] methods = type.getDeclaredMethods(); | ||
| Arrays.sort(methods, ReflectionUtils::compareMethods); | ||
| return methods; | ||
| } |
Code changes:
Sorting is behavior-neutral: member order carries no semantic meaning in these paths; it only affects the byte layout of emitted class files.