[ 
https://issues.apache.org/jira/browse/GROOVY-12149?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18095825#comment-18095825
 ] 

ASF GitHub Bot commented on GROOVY-12149:
-----------------------------------------

paulk-asert commented on PR #2690:
URL: https://github.com/apache/groovy/pull/2690#issuecomment-4956050602

   Claude recommends the following 
   
   Here's the concrete recommendation for #2690 — a single total, spec-stable 
ordering used by both helpers.
   
   ## The comparator
   
   ```java
   /**
    * Orders by name, then parameter types, then return type. This is a total 
order:
    * the JVM forbids two methods in one class sharing a name and descriptor, 
and the
    * descriptor is exactly the parameter types plus the return type.
    */
   private static int compareMethods(final Method m1, final Method m2) {
       int c = m1.getName().compareTo(m2.getName());
       if (c != 0) return c;
       c = compareParameterTypes(m1.getParameterTypes(), 
m2.getParameterTypes());
       if (c != 0) return c;
       return 
m1.getReturnType().getName().compareTo(m2.getReturnType().getName());
   }
   
   private static int compareParameterTypes(final Class<?>[] p1, final 
Class<?>[] p2) {
       int c = Integer.compare(p1.length, p2.length);
       if (c != 0) return c;
       for (int i = 0; i < p1.length; i += 1) {
           c = p1[i].getName().compareTo(p2[i].getName());
           if (c != 0) return c;
       }
       return 0;
   }
   
   public static Method[] getDeclaredMethodsSorted(final Class<?> type) {
       Method[] methods = type.getDeclaredMethods(); // fresh array; safe to 
sort in place
       Arrays.sort(methods, ReflectionUtils::compareMethods);
       return methods;
   }
   
   public static Constructor<?>[] getDeclaredConstructorsSorted(final Class<?> 
type) {
       Constructor<?>[] constructors = type.getDeclaredConstructors();
       // name is always <init> and the return type always void, so the 
parameters decide
       Arrays.sort(constructors, (c1, c2) ->
           compareParameterTypes(c1.getParameterTypes(), 
c2.getParameterTypes()));
       return constructors;
   }
   ```
   
   ## Why this shape
   
   **The return type is mandatory, and it is the whole point.** Leave it out 
and a bridge method ties with the method it bridges.
   
   **Parameters before return type**, so a bridge sorts adjacent to the method 
it bridges and overloads group by signature. 
`CachedMethod.compareToCachedMethod` (`CachedMethod.java:126`) orders name → 
return type → parameters instead; either is total, and it's worth citing as the 
in-repo precedent that *already* includes the return type for exactly this 
reason. Pick one and be consistent.
   
   **Explicit keys rather than `Method::toString`.** The PR's current 
`thenComparing(Method::toString)` is genuinely correct — `toString()` includes 
the return type, so it is total — but its format is unspecified, which means 
byte-identical output across JDK versions rests on an implementation detail, 
and it rebuilds a long string on every comparison. Comparing class names is 
spec-stable and cheaper.
   
   ## And drop the fields
   
   Delete `getDeclaredFieldsSorted` and its use in `configureClassNode`. 
HotSpot returns declared fields in class-file order — it's the methods array it 
keeps sorted by symbol address, which is why methods and constructors vary and 
fields don't. So sorting fields buys no determinism, and it *costs*: 
`GeneralUtils.getAllProperties` walks `getFields()` in order, so alphabetizing 
a precompiled superclass's fields reorders `@TupleConstructor` parameters. It 
also makes the reflection path disagree with the decompiler path, which 
preserves class-file order.
   
   Constructors, by contrast, live in the methods array as `<init>` and 
genuinely do come back in an arbitrary order — so they belong in the fix 
alongside methods.
   




> Nondeterministic reflection order flows into generated bytecode, breaking 
> reproducible builds
> ---------------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12149
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12149
>             Project: Groovy
>          Issue Type: Bug
>            Reporter: James Daugherty
>            Priority: Major
>
> Follow-up to GROOVY-12146, which fixed nondeterministic ordering of 
> annotation members copied from precompiled classes. The same root cause — 
> Class.getDeclaredMethods()/getDeclaredFields()/getDeclaredConstructors() 
> returning members in an unspecified order that varies between HotSpot runs — 
> also affects Java8#configureClassNode, which populates ClassNodes for 
> precompiled classes via reflection. The enumeration order is preserved 
> through to bytecode generation (e.g. via the LinkedHashMap returned by 
> ClassNode#getDeclaredMethodsMap), so two compilations of identical sources on 
> the same JDK can still produce byte-different class files.
> Two manifestations were observed while verifying the reproducibility of the 
> Apache Grails 8.0.0-M3 release artifacts (same sources, same JDK, 
> containerized double-build):
> 1. MOP bridge methods: for classes extending a precompiled Groovy class, 
> MopWriter#getSuperMethods iterates the superclass's declared-methods map, so 
> the synthetic super$N$… methods are emitted in a different order per build.
> 2. Woven trait methods: methods copied from a precompiled trait (e.g. GORM's 
> DirtyCheckable#trackChanges/syncChangedProperties) are woven into 
> implementing classes in enumeration order, reordering the emitted methods and 
> the constant pool.
> In both cases the bytecode is semantically identical — decompiled sources 
> match exactly — only member emission order (and the constant-pool layout that 
> follows from it) differs.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to