You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/08/17 16:35:11 UTC

(groovy) branch master updated: GROOVY-12232: Wrap AIC cast arguments only when the generated constructor delegates to super dynamically

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

daniellansun 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 889ae18448 GROOVY-12232: Wrap AIC cast arguments only when the generated constructor delegates to super dynamically
889ae18448 is described below

commit 889ae184480c61803c39334d763a409b7781d7e8
Author: Paul King <pa...@asert.com.au>
AuthorDate: Mon Aug 17 18:12:10 2026 +1000

    GROOVY-12232: Wrap AIC cast arguments only when the generated constructor delegates to super dynamically
    
    Since 5.0.0-alpha-4 (GROOVY-6285/GROOVY-9244), writeAICCall wraps every
    non-primitive cast argument in a PojoWrapper so the anonymous inner
    class's generated constructor can select among ambiguous super
    constructors by the declared cast type at runtime — the dynamic
    selectConstructorAndTransformArguments path unwraps it. But when the
    super constructor is arity-unique, the generated constructor binds it
    statically and consumes its Object parameter with a plain cast, which
    the wrapper reaches intact: new Test(['a'] as Set) {} failed with
    GroovyCastException 'Cannot cast PojoWrapper to Set' (reduced from
    Gradle's test suite).
    
    writeAICCall now evaluates the same predicate the constructor's own
    compilation uses for its super call (arity-unique candidate, no spread
    arguments — mirroring makeDirectConstructorCall) against the generated
    constructor's super call, found by scanning its body (the this$0
    assignment may precede it), and wraps only when delegation will be
    dynamic. Both consumers now see what they expect: the dynamic selector
    receives wrappers and keeps choosing by cast type; a statically bound
    super call receives the plain value.
---
 .../groovy/classgen/asm/InvocationWriter.java      | 60 ++++++++++++-
 src/test/groovy/bugs/Groovy12232.groovy            | 99 ++++++++++++++++++++++
 2 files changed, 158 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/InvocationWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/InvocationWriter.java
index 598c4669c0..2471e038cf 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/InvocationWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/InvocationWriter.java
@@ -37,6 +37,9 @@ import org.codehaus.groovy.ast.expr.SpreadExpression;
 import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
 import org.codehaus.groovy.ast.expr.TupleExpression;
 import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.ExpressionStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
 import org.codehaus.groovy.ast.tools.WideningCategories;
 import org.codehaus.groovy.classgen.AsmClassGenerator;
 import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
@@ -752,6 +755,14 @@ public class InvocationWriter {
 
         String ownerDescriptor = prepareConstructorCall(ctor);
 
+        // Cast arguments carry their cast type in a wrapper only when the
+        // generated constructor delegates to super through the dynamic
+        // selection path, which unwraps them (GROOVY-6285, GROOVY-9244). A
+        // statically bound super call consumes its arguments with a plain
+        // cast, so a wrapper would reach that cast intact and fail there
+        // (GROOVY-12232).
+        boolean wrapCastArguments = delegatesToSuperDynamically(ctor);
+
         List<Expression> args = makeArgumentList(call.getArguments()).getExpressions();
         // if a this appears as parameter here, then it should be
         // not static, unless we are in a static method. But since
@@ -767,7 +778,7 @@ public class InvocationWriter {
                 loadVariableWithReference(var);
             } else {
                 arg.visit(controller.getAcg());
-                if (arg instanceof CastExpression && !isPrimitiveType(arg.getType())) {
+                if (wrapCastArguments && arg instanceof CastExpression && !isPrimitiveType(arg.getType())) {
                     controller.getAcg().loadWrapper(arg); // GROOVY-6285, GROOVY-9244
                 }
             }
@@ -778,6 +789,53 @@ public class InvocationWriter {
         return true;
     }
 
+    /**
+     * Whether the given generated constructor (of an anonymous inner class)
+     * will delegate to its super constructor through the dynamic
+     * {@code selectConstructorAndTransformArguments} path rather than a direct
+     * {@code invokespecial}. Mirrors the choice made when the constructor
+     * itself is compiled: {@link #makeDirectConstructorCall} binds directly
+     * only for a spread-free argument list with an arity-unique candidate.
+     */
+    private static boolean delegatesToSuperDynamically(final ConstructorNode ctor) {
+        // the generated constructor's super call is not necessarily the first
+        // statement (the this$0 assignment may precede it)
+        ConstructorCallExpression superCall = null;
+        Statement code = ctor.getCode();
+        List<Statement> statements = code instanceof BlockStatement block
+                ? block.getStatements() : List.of(code);
+        for (Statement statement : statements) {
+            if (statement instanceof ExpressionStatement es
+                    && es.getExpression() instanceof ConstructorCallExpression cce
+                    && cce.isSuperCall()) {
+                superCall = cce;
+                break;
+            }
+        }
+        if (superCall == null) {
+            return false; // no super delegation to select for
+        }
+        List<Expression> superArgs = makeArgumentList(superCall.getArguments()).getExpressions();
+        for (Expression arg : superArgs) {
+            if (arg instanceof SpreadExpression) return true;
+        }
+        int nArguments = superArgs.size();
+        ConstructorNode match = null, varg = null;
+        for (ConstructorNode constructor : ctor.getDeclaringClass().getSuperClass().getDeclaredConstructors()) {
+            int nParameters = constructor.getParameters().length;
+            if (nArguments == nParameters) {
+                if (match == null) match = constructor;
+                else return true; // ambiguous match
+            } else if (isVargs(constructor.getParameters())
+                    && (nArguments == nParameters - 1 || nArguments > nParameters)) {
+                if (varg == null) varg = constructor;
+                else return true; // ambiguous match
+            }
+        }
+        if (match == null) match = varg;
+        return match == null;
+    }
+
     private void loadVariableWithReference(final VariableExpression var) {
         if (!var.isUseReferenceDirectly()) {
             var.visit(controller.getAcg());
diff --git a/src/test/groovy/bugs/Groovy12232.groovy b/src/test/groovy/bugs/Groovy12232.groovy
new file mode 100644
index 0000000000..31012e75da
--- /dev/null
+++ b/src/test/groovy/bugs/Groovy12232.groovy
@@ -0,0 +1,99 @@
+/*
+ *  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 bugs
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+/**
+ * GROOVY-12232: an {@code as} coercion passed directly to an anonymous inner
+ * class constructor must not leak a {@code PojoWrapper} into the generated
+ * constructor when the super constructor is statically bound.
+ */
+final class Groovy12232 {
+
+    @Test
+    void testCoercionArgumentWithStaticallyBoundSuper() {
+        assertScript '''
+            class T {
+                Set<String> values
+                T(Set<String> values) { this.values = values }
+            }
+            def t = new T(['a'] as Set) {}
+            assert t.values == ['a'] as Set
+        '''
+    }
+
+    @Test
+    void testCoercionArgumentAmongOtherArguments() {
+        assertScript '''
+            class T {
+                def a, b
+                T(String a, Set<String> b) { this.a = a; this.b = b }
+            }
+            def t = new T('x', ['a'] as Set) {}
+            assert t.a == 'x'
+            assert t.b == ['a'] as Set
+        '''
+    }
+
+    @Test
+    void testCoercionArgumentWithAmbiguousSuperStillSelectsByCastType() {
+        // GROOVY-9244: with arity-ambiguous super constructors the generated
+        // constructor delegates dynamically and the cast type must still drive
+        // constructor selection — the wrapper remains required on this path
+        assertScript '''
+            class T {
+                def picked
+                T(Set<String> values)  { picked = 'set' }
+                T(List<String> values) { picked = 'list' }
+            }
+            def t1 = new T(['a'] as Set) {}
+            assert t1.picked == 'set'
+            def t2 = new T(['a'] as List) {}
+            assert t2.picked == 'list'
+        '''
+    }
+
+    @Test
+    void testCoercionArgumentWithVargsSuper() {
+        assertScript '''
+            class T {
+                def values
+                T(Set<String>... values) { this.values = values }
+            }
+            def t = new T(['a'] as Set) {}
+            assert t.values[0] == ['a'] as Set
+        '''
+    }
+
+    @Test
+    void testCoercionThroughLocalVariableControl() {
+        assertScript '''
+            class T {
+                Set<String> values
+                T(Set<String> values) { this.values = values }
+            }
+            def s = ['a'] as Set
+            def t = new T(s) {}
+            assert t.values == ['a'] as Set
+        '''
+    }
+}