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
+ '''
+ }
+}