Paul King created GROOVY-12157:
----------------------------------

             Summary: Covariant bridge methods are emitted in hash order, not 
the order they are found
                 Key: GROOVY-12157
                 URL: https://issues.apache.org/jira/browse/GROOVY-12157
             Project: Groovy
          Issue Type: Improvement
            Reporter: Paul King
            Assignee: Paul King


h2. Description

{{Verifier.addCovariantMethods}} collects the bridge methods it needs to 
generate into unordered maps and then iterates them to emit the methods:

{code:java}
// Verifier.java:1679-1680
Map<String, MethodNode> absInterfaceMethods = new HashMap<>();
Map<String, MethodNode> allInterfaceMethods = new HashMap<>();
...
// Verifier.java:1719
Map<String, MethodNode> methodsToAdd = new HashMap<>();
addCovariantMethods(classNode, declaredMethods, absInterfaceMethods, 
methodsToAdd, genericsSpec);

if (!methodsToAdd.isEmpty()) {
    ...
    for (Map.Entry<String, MethodNode> entry : methodsToAdd.entrySet()) {
        // we skip bridge methods implemented in current class already
        MethodNode mn = declaredMethodsMap.get(entry.getKey());
        if (mn == null || !mn.getDeclaringClass().equals(classNode)) {
            addPropertyMethod(entry.getValue());   // <-- emits into the class 
file
        }
    }
}
{code}

The three maps chain into each other — {{allInterfaceMethods}} is iterated to 
populate {{absInterfaceMethods}} ({{:1699}}), which is iterated as 
{{interfaceMethods.values()}} ({{:1750}}) to populate {{methodsToAdd}}, which 
is iterated to call {{addPropertyMethod}}. So *the order of the generated 
bridge methods in the emitted class file is the hash order of the 
type-descriptor keys*, not the order in which the methods were discovered.

h3. Demonstration

{code:java}
interface Alpha   { Object alpha()   }
interface Beta    { Object beta()    }
interface Gamma   { Object gamma()   }
interface Delta   { Object delta()   }
interface Epsilon { Object epsilon() }

class Sample implements Alpha, Beta, Gamma, Delta, Epsilon {
    String  alpha()   { null }
    Integer beta()    { null }
    Long    gamma()   { null }
    Double  delta()   { null }
    Short   epsilon() { null }
}
{code}

{{javap -p Sample.class}} shows the five declared methods in declaration order, 
followed by the five generated bridges in an arbitrary order:

{noformat}
public java.lang.String alpha();
public java.lang.Integer beta();
public java.lang.Long gamma();
public java.lang.Double delta();
public java.lang.Short epsilon();
public java.lang.Object alpha();     <-- bridges, in hash order:
public java.lang.Object gamma();         alpha, gamma, epsilon, beta, delta
public java.lang.Object epsilon();
public java.lang.Object beta();
public java.lang.Object delta();
{noformat}

h3. Impact

The keys are type-descriptor {{String}}s and {{String.hashCode}} is stable, so 
for a given key set the iteration order is reproducible across JVM runs — this 
is not, by itself, a live reproducible-builds failure. Two reasons it still 
matters:

* The *insertion* order into these maps comes from {{ClassNode.getMethods()}} 
of the supertypes, which for reflection-configured {{ClassNode}}s is the 
unspecified JDK reflection order being addressed in GROOVY-12149. Within a 
colliding bucket, {{HashMap}} chain order follows insertion order, so the 
emitted bridge order is coupled to that nondeterminism.
* Even where it is stable, the emitted order is arbitrary rather than 
meaningful, which makes generated class files harder to diff and reason about.

h3. Proposed fix

Use insertion-ordered maps for the three that are iterated to produce output:

{code:java}
Map<String, MethodNode> absInterfaceMethods = new LinkedHashMap<>();
Map<String, MethodNode> allInterfaceMethods = new LinkedHashMap<>();
...
Map<String, MethodNode> methodsToAdd = new LinkedHashMap<>();
{code}

Bridge methods are then emitted in the order they are discovered (declaration 
order in the example above: {{alpha, beta, gamma, delta, epsilon}}). The method 
*set* is unchanged — only the order.

{{declaredMethodsMap}} ({{:1724}}) is lookup-only and {{defaultMethods}} 
({{:323}}) feeds duplicate-default-method diagnostics only, so neither needs to 
change.

h3. Test

{{src/test/groovy/org/codehaus/groovy/classgen/CovariantBridgeMethodOrderTest.groovy}}
 compiles the source above via {{CompilationUnit}}, reads the emitted bytes 
with an ASM {{ClassReader}} (the pattern already used by 
{{Groovy11776.groovy}}), and asserts the bridge methods appear in declaration 
order. It fails on the current code with {{['alpha', 'gamma', 'epsilon', 
'beta', 'delta']}} and passes with the fix.

h3. Related

GROOVY-12149 (nondeterministic reflection order flows into generated bytecode) 
— feeds the insertion order of these maps. GROOVY-12156 (chooseBestMethod 
returns candidates in identity-hash order) — same class of defect in the static 
type checker.



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

Reply via email to