From e35f96db3076113fc9c45a3e02306a99fa734d5f Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Sat, 24 Oct 2026 11:16:49 -0700 Subject: [PATCH 01/15] GROOVY-12488: SC: call the constructor of a constructor reference directly Under static compilation, a constructor reference such as Made::new is implemented by a synthetic method of the enclosing class, which the LambdaMetafactory call site targets. That method was generated with STATIC_COMPILE_NODE set to false, because its constructor call had no target constructor. So it created the object through the meta-class, an "init" invokedynamic call site followed by a "cast" call site, and the constructor was selected at run time on every call. In a GraalVM native image, the constructor also had to be registered for reflection although the code was statically compiled. StaticTypesMethodReferenceExpressionWriter now selects the constructor when it builds the synthetic method: chooseBestMethod matches the declared constructors of the type against the parameter types of the functional interface, as the arguments of a constructor call are matched. When a single constructor matches and the current class can call it, it becomes the DIRECT_METHOD_CALL_TARGET of the constructor call and the synthetic method is statically compiled, so StaticInvocationWriter emits NEW and INVOKESPECIAL, with any boxing, unboxing, widening or variadic array the arguments need. As for other method references, the constructor is then chosen by the parameter types of the functional interface, not by the run-time types of the arguments. The constructor is still selected at run time when no single constructor accepts the parameter types, for example Foo::new as a Function when Foo only declares Foo(String), and when the constructor is protected and declared in another package, which only a super(...) call can use. Array constructor references are unchanged: they already create the array directly. --- ...cTypesMethodReferenceExpressionWriter.java | 45 +- .../transform/stc/MethodReferenceTest.groovy | 406 +++++++++++++++++- 2 files changed, 442 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java index 2afdfbf0731..8540ce61518 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java @@ -21,6 +21,7 @@ import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ConstructorNode; import org.codehaus.groovy.ast.GenericsType; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.Parameter; @@ -49,6 +50,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.ToIntFunction; @@ -69,6 +71,7 @@ import static org.codehaus.groovy.classgen.asm.sc.StaticTypesFunctionalInterfaceMetadataKey.METHOD_REFERENCE_DESERIALIZE_METHOD_NAME; import static org.codehaus.groovy.runtime.ArrayGroovyMethods.last; import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.allParametersAndArgumentsMatch; +import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.chooseBestMethod; import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.filterMethodsByVisibility; import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.findDGMMethodsForClassNode; import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.implementsInterfaceOrIsSubclassOf; @@ -460,22 +463,56 @@ private MethodNode addSyntheticMethodForConstructorReference(final String method ArgumentListExpression ctorArgs = new ArgumentListExpression(parametersWithExactType); Expression returnValue; + boolean staticCompile = false; if (returnType.isArray()) { returnValue = new ArrayExpression( returnType.getComponentType(), null, ctorArgs.getExpressions()); } else { returnValue = ctorX(returnType, ctorArgs); + ConstructorNode ctor = findConstructorReferenceTarget(returnType, parametersWithExactType); + if (ctor != null) { + returnValue.putNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET, ctor); + staticCompile = true; + } } MethodNode delegateMethod = addSyntheticMethod(methodName, returnType, returnValue, parametersWithExactType, ClassNode.EMPTY_ARRAY); - // TODO: if StaticTypesMarker.DIRECT_METHOD_CALL_TARGET or - // OptimizingStatementWriter.StatementMeta.class metadatas - // can bet set for the ctorX above, then this can be TRUE: - delegateMethod.putNodeMetaData(StaticCompilationMetadataKeys.STATIC_COMPILE_NODE, Boolean.FALSE); + // without a target constructor, the constructor is selected at run time + delegateMethod.putNodeMetaData(StaticCompilationMetadataKeys.STATIC_COMPILE_NODE, staticCompile); return delegateMethod; } + /** + * Selects the constructor of {@code type} that accepts the parameter types + * of the functional interface, the way the constructor of a constructor call + * expression is selected. + * + * @return the constructor, or {@code null} if no single constructor matches + * or the matching constructor cannot be called directly + */ + private ConstructorNode findConstructorReferenceTarget(final ClassNode type, final Parameter[] samParameters) { + if (type.isInterface() || type.isAbstract() || type.isEnum()) return null; + + ClassNode[] argumentTypes = Arrays.stream(samParameters).map(Parameter::getType).toArray(ClassNode[]::new); + List ctors = chooseBestMethod(type, new ArrayList<>(type.getDeclaredConstructors()), argumentTypes); + if (ctors.size() == 1 && ctors.get(0) instanceof ConstructorNode ctor && isDirectlyAccessible(ctor)) { + return ctor; + } + return null; + } + + /** + * Determines if the bytecode of the current class can call the constructor. + */ + private boolean isDirectlyAccessible(final ConstructorNode ctor) { + ClassNode declaringClass = ctor.getDeclaringClass(), accessingClass = controller.getClassNode(); + if (ctor.isProtected()) { // only super(...) can call a protected constructor of another package + return Objects.equals(declaringClass.getPackageName(), accessingClass.getPackageName()); + } + return AsmClassGenerator.isMemberDirectlyAccessible(ctor.getModifiers(), declaringClass, accessingClass); + } + private MethodNode addSyntheticMethod(final String methodName, final ClassNode returnType, final Expression returnValue, final Parameter[] parameters, final ClassNode[] exceptions) { return controller.getClassNode().addSyntheticMethod( methodName, diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index eca241769bb..f763451449a 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -19,6 +19,7 @@ package groovy.transform.stc import org.codehaus.groovy.classgen.asm.AbstractBytecodeTestCase +import org.codehaus.groovy.classgen.asm.InstructionSequence import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -1146,6 +1147,218 @@ final class MethodReferenceTest { assert err.message =~ /Cannot call java.lang.Integer\[\]#\(int\) with arguments / } + @Test // class::new + void testFunctionCN10() { + assertScript shell, ''' + class Over { + final String how + Over(String s) { how = 'String' } + Over(Integer i) { how = 'Integer' } + Over(long l) { how = 'long' } + Over(int i, long j) { how = 'int,long' } + } + + @CompileStatic + void test() { + Function s = Over::new + assert s.apply('x').how == 'String' + Function i = Over::new + assert i.apply(1).how == 'Integer' + IntFunction p = Over::new // like new Over(int) + assert p.apply(1).how == 'Integer' + LongFunction l = Over::new + assert l.apply(1L).how == 'long' + BiFunction b = Over::new + assert b.apply(1, 2).how == 'int,long' + } + + test() + ''' + } + + @Test // class::new + void testFunctionCN11() { + assertScript shell, ''' + class Var { + final List values + Var(String... values) { this.values = values.toList() } + } + + @CompileStatic + void test() { + Supplier none = Var::new + assert none.get().values == [] + Function one = Var::new + assert one.apply('a').values == ['a'] + BiFunction two = Var::new + assert two.apply('a', 'b').values == ['a', 'b'] + Function array = Var::new + assert array.apply(['a', 'b', 'c'] as String[]).values == ['a', 'b', 'c'] + } + + test() + ''' + } + + @Test // class::new + void testFunctionCN12() { + assertScript shell, ''' + class Pair { + final A a + final B b + Pair(A a, B b) { this.a = a; this.b = b } + } + + @CompileStatic + void test() { + BiFunction> f = Pair::new + Pair pair = f.apply('x', 1) + assert pair.a == 'x' && pair.b == 1 + + Function> g = ArrayList::new // ArrayList(int) + assert g.apply(10).isEmpty() + Function, List> h = ArrayList::new // ArrayList(Collection) + assert h.apply(['y']) == ['y'] + } + + test() + ''' + } + + @Test // class::new + void testFunctionCN13() { + assertScript shell, ''' + @CompileStatic + class C { + private final String value + private C(String value) { this.value = value } + + static class Box { + private final String value + private Box(String value) { this.value = value } + static Function outer() { C::new } + } + + static Function inner() { Box::new } + static Function inClosure() { + def make = { -> Function f = Box::new; f } + make() + } + static Function inLambda() { + Supplier> make = () -> { Function f = Box::new; f } + make.get() + } + } + + assert C.inner().apply('i').value == 'i' + assert C.Box.outer().apply('o').value == 'o' + assert C.inClosure().apply('c').value == 'c' + assert C.inLambda().apply('l').value == 'l' + ''' + } + + @Test // class::new + void testFunctionCN14() { + assertScript shell, ''' + class Outer { + class Inner { + final Outer outer + Inner() { outer = Outer.this } + } + } + + @CompileStatic + void test() { + Function f = Outer.Inner::new + def outer = new Outer() + assert f.apply(outer).outer.is(outer) + } + + test() + ''' + } + + @Test // class::new + void testFunctionCN15() { + assertScript shell, ''' + class Dflt { + final String value + Dflt(String a, String b = 'b') { value = a + b } + } + + @CompileStatic + void test() { + Function f = Dflt::new + assert f.apply('a').value == 'ab' + BiFunction g = Dflt::new + assert g.apply('a', 'c').value == 'ac' + } + + test() + ''' + } + + // GROOVY-10971 + @Test // class::new + void testFunctionCN16() { + assertScript shell, ''' + class Foo { + final String value + Foo(String s) { value = s } + } + + @CompileStatic + void test() { + Function f = Foo::new // no Foo(Object); selected at run time + assert f.apply('x').value == 'x' + } + + test() + ''' + } + + @Test // class::new + void testFunctionCN17() { + assertScript shell, '''package p + class A { + final String value + protected A(String value) { this.value = value } + } + + true + ''' + assertScript shell, ''' + @CompileStatic + class C extends p.A { + C() { super('C') } + static Function create() { + p.A::new // protected constructor of another package + } + } + + assert C.create().apply('x').value == 'x' + ''' + } + + @Test // class::new + void testFunctionCN18() { + assertScript shell, ''' + class X { + final String how + X(CharSequence cs) { how = 'CharSequence' } + X(String s) { how = 'String' } + } + + @CompileStatic + void test() { + Function f = X::new // selected by parameter type of Function#apply, as in Java + assert f.apply('x').how == 'CharSequence' + } + + test() + ''' + } + @Test // arrayClass::new void testIntFunctionCN() { assertScript shell, ''' @@ -2698,15 +2911,16 @@ final class MethodReferenceTest { } ''' - def helperBytecode = compileStaticBytecode(classNamePattern: 'C', method: 'ctorRef$create$0', script) + def helperBytecode = compileStaticMethodBytecode('ctorRef$create$0', script) assert helperBytecode.hasSequence([ 'private final static synthetic ctorRef$create$0(Ljava/lang/String;)LC$Box;', - 'LDC LC$Box;.class', - 'INVOKEDYNAMIC init(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Object;', - '""', - 'INVOKEDYNAMIC cast(Ljava/lang/Object;)LC$Box;', + 'NEW C$Box', + 'DUP', + 'ALOAD 0', + 'INVOKESPECIAL C$Box. (Ljava/lang/String;)V', 'ARETURN' ]) + assertNoDynamicConstructorCall(helperBytecode) def outerBytecode = compileStaticBytecode(classNamePattern: 'C', method: 'create', script) assertUsesInvokeDynamicFactory( @@ -2717,6 +2931,176 @@ final class MethodReferenceTest { ) } + @Test + void testConstructorReferenceCallsNoArgConstructorDirectly() { + def helperBytecode = compileStaticMethodBytecode('ctorRef$create$0', ''' + class Made { + Made() { } + } + @CompileStatic + class C { + static Supplier create() { + Made::new + } + } + ''') + + assert helperBytecode.hasStrictSequence([ + 'NEW Made', + 'DUP', + 'INVOKESPECIAL Made. ()V', + 'ARETURN' + ]) + assertNoDynamicConstructorCall(helperBytecode) + } + + @Test + void testConstructorReferenceSelectsConstructorBySamParameterTypes() { + def script = ''' + class Over { + Over(String s) { } + Over(Integer i) { } + Over(long l) { } + Over(int i, long j) { } + } + @CompileStatic + class C { + static Function string() { + Over::new + } + static Function integer() { + Over::new + } + static LongFunction primitive() { + Over::new + } + static BiFunction unboxAndWiden() { + Over::new + } + } + ''' + + def string = compileStaticMethodBytecode('ctorRef$string$0', script) + assert string.hasSequence(['ALOAD 0', 'INVOKESPECIAL Over. (Ljava/lang/String;)V']) + assertNoDynamicConstructorCall(string) + + def integer = compileStaticMethodBytecode('ctorRef$integer$1', script) + assert integer.hasSequence(['ALOAD 0', 'INVOKESPECIAL Over. (Ljava/lang/Integer;)V']) + assertNoDynamicConstructorCall(integer) + + def primitive = compileStaticMethodBytecode('ctorRef$primitive$2', script) + assert primitive.hasSequence(['LLOAD 0', 'INVOKESPECIAL Over. (J)V']) + assertNoDynamicConstructorCall(primitive) + + def unboxAndWiden = compileStaticMethodBytecode('ctorRef$unboxAndWiden$3', script) + assert unboxAndWiden.hasSequence(['ALOAD 0', 'ALOAD 1', 'INVOKESPECIAL Over. (IJ)V']) + assertNoDynamicConstructorCall(unboxAndWiden) + } + + @Test + void testVariadicConstructorReferenceCallsConstructorDirectly() { + def script = ''' + class Var { + Var(String... values) { } + } + @CompileStatic + class C { + static Supplier none() { + Var::new + } + static Function one() { + Var::new + } + static BiFunction two() { + Var::new + } + static Function array() { + Var::new + } + } + ''' + + ['none', 'one', 'two'].eachWithIndex { name, i -> + def bytecode = compileStaticMethodBytecode("ctorRef\$${name}\$${i}", script) + assert bytecode.hasSequence(['NEW Var', 'ANEWARRAY java/lang/String', 'INVOKESPECIAL Var. ([Ljava/lang/String;)V']) + assertNoDynamicConstructorCall(bytecode) + } + + def array = compileStaticMethodBytecode('ctorRef$array$3', script) + assert array.hasSequence(['NEW Var', 'ALOAD 0', 'INVOKESPECIAL Var. ([Ljava/lang/String;)V']) + assert !array.hasSequence(['ANEWARRAY']) + assertNoDynamicConstructorCall(array) + } + + @Test + void testGenericClassConstructorReferenceCallsConstructorDirectly() { + def bytecode = compileStaticMethodBytecode('ctorRef$create$0', ''' + class Gen { + Gen(T value) { } + } + @CompileStatic + class C { + static Function> create() { + Gen::new + } + } + ''') + + assert bytecode.hasSequence(['NEW Gen', 'ALOAD 0', 'INVOKESPECIAL Gen. (Ljava/lang/Object;)V']) + assertNoDynamicConstructorCall(bytecode) + } + + @Test + void testPrivateConstructorOfNestmateIsCalledDirectly() { + def bytecode = compileStaticMethodBytecode('ctorRef$create$0', ''' + @CompileStatic + class C { + static class Box { + private Box(String value) { } + } + static Function create() { + Box::new + } + } + ''') + + assert bytecode.hasSequence(['NEW C$Box', 'ALOAD 0', 'INVOKESPECIAL C$Box. (Ljava/lang/String;)V']) + assertNoDynamicConstructorCall(bytecode) + } + + // GROOVY-10971: no constructor accepts the parameter type of the functional interface + @Test + void testConstructorReferenceWithoutMatchingConstructorSelectsConstructorAtRuntime() { + def bytecode = compileStaticMethodBytecode('ctorRef$create$0', ''' + class Foo { + Foo(String s) { } + } + @CompileStatic + class C { + static Function create() { + Foo::new + } + } + ''') + + assert bytecode.hasSequence(['INVOKEDYNAMIC init(Ljava/lang/Class;Ljava/lang/Object;)Ljava/lang/Object;']) + } + + @Test + void testProtectedConstructorOfAnotherPackageIsSelectedAtRuntime() { + def bytecode = compileStaticMethodBytecode('ctorRef$create$0', ''' + @CompileStatic + class C extends ObjectInputStream { + C() { super() } + static Supplier create() { + ObjectInputStream::new // protected ObjectInputStream() + } + } + ''') + + assert bytecode.hasSequence(['INVOKEDYNAMIC init(Ljava/lang/Class;)Ljava/lang/Object;']) + } + @Test void testArrayConstructorReferenceUsesSyntheticArrayFactoryMethod() { def script = ''' @@ -2803,10 +3187,22 @@ final class MethodReferenceTest { assert !bytecode.hasSequence(['INVOKESTATIC org/codehaus/groovy/runtime/ScriptBytecodeAdapter.getMethodPointer']) } + private static void assertNoDynamicConstructorCall(final bytecode) { + assert !bytecode.hasSequence(['INVOKEDYNAMIC init(']) + assert !bytecode.hasSequence(['INVOKEDYNAMIC cast(']) + } + private compileStaticBytecode(final Map options = [:], final String script) { compile(options, COMMON_IMPORTS + script) } + private InstructionSequence compileStaticMethodBytecode(final String method, final String script) { + List instructions = compileStaticBytecode(classNamePattern: 'C', method: method, script).instructions + int begin = instructions.indexOf('--BEGIN--'), end = instructions.indexOf('--END--') + assert begin >= 0 && end > begin : "method $method not found" + new InstructionSequence(instructions: instructions[(begin + 1).. Date: Sat, 24 Oct 2026 18:01:17 -0700 Subject: [PATCH 02/15] GROOVY-12488: select the constructor of a constructor reference among the visible ones The constructor of a statically compiled constructor reference was chosen from all the constructors the type declares. When the best match was a constructor the enclosing class cannot use, such as private Foo(String) next to a public Foo(Object), the reference fell back to run-time selection and the meta-class called the inaccessible constructor, while new Foo(s) in the same class calls Foo(Object). The constructors are now filtered by visibility before chooseBestMethod, as the type checker does for a constructor call, so the reference calls Foo(Object) directly, as new Foo(s) and Java do. Visibility is checked for the enclosing class of a closure or lambda, not for the generated class, so a private constructor of a nested class stays visible from a closure or lambda of the outer class, as it is for the type checker. --- ...cTypesMethodReferenceExpressionWriter.java | 10 +- .../transform/stc/MethodReferenceTest.groovy | 161 +++++++++++++++++- 2 files changed, 166 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java index 8540ce61518..7cafd27c498 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java @@ -485,17 +485,21 @@ private MethodNode addSyntheticMethodForConstructorReference(final String method /** * Selects the constructor of {@code type} that accepts the parameter types - * of the functional interface, the way the constructor of a constructor call - * expression is selected. + * of the functional interface, among the constructors visible from the + * enclosing class, as the type checker selects the constructor of a + * constructor call. * * @return the constructor, or {@code null} if no single constructor matches * or the matching constructor cannot be called directly */ private ConstructorNode findConstructorReferenceTarget(final ClassNode type, final Parameter[] samParameters) { + // the type checker rejects these, unless a type checking extension suppresses the error if (type.isInterface() || type.isAbstract() || type.isEnum()) return null; ClassNode[] argumentTypes = Arrays.stream(samParameters).map(Parameter::getType).toArray(ClassNode[]::new); - List ctors = chooseBestMethod(type, new ArrayList<>(type.getDeclaredConstructors()), argumentTypes); + // in a closure or lambda, visibility is checked for the enclosing class, as the type checker does + List visible = filterMethodsByVisibility(new ArrayList<>(type.getDeclaredConstructors()), controller.getThisType()); + List ctors = chooseBestMethod(type, visible, argumentTypes); if (ctors.size() == 1 && ctors.get(0) instanceof ConstructorNode ctor && isDirectlyAccessible(ctor)) { return ctor; } diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index f763451449a..dd6e40b7169 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -20,11 +20,15 @@ package groovy.transform.stc import org.codehaus.groovy.classgen.asm.AbstractBytecodeTestCase import org.codehaus.groovy.classgen.asm.InstructionSequence +import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.Phases import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource +import java.util.regex.Pattern + import static groovy.test.GroovyAssert.assertScript import static groovy.test.GroovyAssert.shouldFail @@ -1359,6 +1363,73 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 + @Test // class::new + void testFunctionCN19() { + assertScript shell, ''' + class Foo { + final String how + private Foo(String s) { how = 'private String' } + Foo(Object o) { how = 'public Object' } + } + + @CompileStatic + void test() { + Function f = Foo::new // like new Foo(s): Foo(String) is not visible + assert f.apply('x').how == 'public Object' + assert new Foo('x').how == 'public Object' + } + + test() + ''' + } + + // GROOVY-12488 + @Test // class::new + void testFunctionCN20() { + assertScript shell, '''package p + class A { + final String how + @PackageScope A(String s) { how = 'package-private String' } + A(Object o) { how = 'public Object' } + } + + true + ''' + assertScript shell, ''' + @CompileStatic + void test() { + Function f = p.A::new // like new p.A(s): A(String) is not visible + assert f.apply('x').how == 'public Object' + assert new p.A('x').how == 'public Object' + } + + test() + ''' + } + + // GROOVY-12488 + @Test // class::new + void testFunctionCN21() { + assertScript shell, ''' + class IL { + final String how + IL(Object o) { how = 'Object' } + IL(long l) { how = 'long' } + } + + @CompileStatic + void test() { + Function f = IL::new // like new IL(i) for Integer i + assert f.apply(1).how == 'Object' + Integer i = 1 + assert new IL(i).how == 'Object' + } + + test() + ''' + } + @Test // arrayClass::new void testIntFunctionCN() { assertScript shell, ''' @@ -3068,6 +3139,73 @@ final class MethodReferenceTest { assertNoDynamicConstructorCall(bytecode) } + // GROOVY-12488 + @Test + void testPrivateConstructorOfNestedClassIsCalledDirectlyFromClosureOrLambda() { + def script = ''' + @CompileStatic + class C { + static class Box { + private Box(String value) { } + } + static Function inClosure() { + def make = { -> Function f = Box::new; f } + make() + } + static Function inLambda() { + Supplier> make = () -> { Function f = Box::new; f } + make.get() + } + } + ''' + + def closure = compileStaticMethodBytecode('ctorRef$doCall$0', script, 'C$_inClosure_closure1') + assert closure.hasSequence(['NEW C$Box', 'ALOAD 0', 'INVOKESPECIAL C$Box. (Ljava/lang/String;)V']) + assertNoDynamicConstructorCall(closure) + + def lambda = compileStaticMethodBytecode('ctorRef$doCall$1', script, 'C$_inLambda_lambda2') + assert lambda.hasSequence(['NEW C$Box', 'ALOAD 0', 'INVOKESPECIAL C$Box. (Ljava/lang/String;)V']) + assertNoDynamicConstructorCall(lambda) + } + + // GROOVY-12488 + @Test + void testConstructorReferenceSelectsConstructorVisibleFromEnclosingClass() { + def bytecode = compileStaticMethodBytecode('ctorRef$create$0', ''' + class Foo { + private Foo(String s) { } + Foo(Object o) { } + } + @CompileStatic + class C { + static Function create() { + Foo::new + } + } + ''') + + assert bytecode.hasSequence(['NEW Foo', 'ALOAD 0', 'INVOKESPECIAL Foo. (Ljava/lang/Object;)V']) + assertNoDynamicConstructorCall(bytecode) + + bytecode = compileStaticMethodBytecodeAgainstLibrary('''package p + class A { + @groovy.transform.PackageScope A(String s) { } + A(Object o) { } + } + ''', 'C', 'ctorRef$create$0', ''' + import java.util.function.Function + @groovy.transform.CompileStatic + class C { + static Function create() { + p.A::new + } + } + ''') + + assert bytecode.hasSequence(['NEW p/A', 'ALOAD 0', 'INVOKESPECIAL p/A. (Ljava/lang/Object;)V']) + assertNoDynamicConstructorCall(bytecode) + } + // GROOVY-10971: no constructor accepts the parameter type of the functional interface @Test void testConstructorReferenceWithoutMatchingConstructorSelectsConstructorAtRuntime() { @@ -3196,8 +3334,27 @@ final class MethodReferenceTest { compile(options, COMMON_IMPORTS + script) } - private InstructionSequence compileStaticMethodBytecode(final String method, final String script) { - List instructions = compileStaticBytecode(classNamePattern: 'C', method: method, script).instructions + private InstructionSequence compileStaticMethodBytecode(final String method, final String script, final String className = 'C') { + selectMethod(method, compileStaticBytecode(classNamePattern: Pattern.quote(className), method: method, script)) + } + + /** + * Compiles {@code script} against the classes of {@code library}, which + * are compiled before, by another compilation unit and class loader. + */ + private InstructionSequence compileStaticMethodBytecodeAgainstLibrary(final String library, final String className, final String method, final String script) { + def loader = new GroovyClassLoader() + loader.parseClass(library) + def unit = new CompilationUnit(null, null, loader) + unit.addSource('script', script) + unit.compile(Phases.CLASS_GENERATION) + def generated = unit.classes.find { it.name == className } + assert generated != null : "class $className not found" + selectMethod(method, extractSequence(generated.bytes, [method: method])) + } + + private static InstructionSequence selectMethod(final String method, final InstructionSequence sequence) { + List instructions = sequence.instructions int begin = instructions.indexOf('--BEGIN--'), end = instructions.indexOf('--END--') assert begin >= 0 && end > begin : "method $method not found" new InstructionSequence(instructions: instructions[(begin + 1).. Date: Sat, 24 Oct 2026 18:05:34 -0700 Subject: [PATCH 03/15] GROOVY-12488: call a protected or package-private constructor directly only within the compilation unit A constructor reference called a protected or package-private constructor of the same package directly. The JVM checks the run-time package, which is the package name and the class loader, so the call failed with IllegalAccessError when the class came from another class loader with the same package name: a class of an earlier GroovyShell parse, which GroovyClassLoader defines with another InnerLoader, or a precompiled class used from a script. Such a constructor is now called directly only when its class is compiled in the same compilation unit, which defines all its classes with one class loader. Otherwise the constructor is selected at run time, as before the constructor reference was compiled statically. --- ...cTypesMethodReferenceExpressionWriter.java | 12 +- .../transform/stc/MethodReferenceTest.groovy | 104 ++++++++++++++++++ 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java index 7cafd27c498..aba8bbe1b82 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java @@ -50,12 +50,12 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.function.ToIntFunction; import static java.util.Comparator.comparingInt; import static java.util.stream.Collectors.joining; +import static org.apache.groovy.ast.tools.ClassNodeUtils.samePackageName; import static org.codehaus.groovy.ast.tools.GeneralUtils.callX; import static org.codehaus.groovy.ast.tools.GeneralUtils.classX; import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; @@ -510,11 +510,15 @@ private ConstructorNode findConstructorReferenceTarget(final ClassNode type, fin * Determines if the bytecode of the current class can call the constructor. */ private boolean isDirectlyAccessible(final ConstructorNode ctor) { + if (ctor.isPublic()) return true; ClassNode declaringClass = ctor.getDeclaringClass(), accessingClass = controller.getClassNode(); - if (ctor.isProtected()) { // only super(...) can call a protected constructor of another package - return Objects.equals(declaringClass.getPackageName(), accessingClass.getPackageName()); + if (ctor.isPrivate()) { // nestmates can call a private constructor + return AsmClassGenerator.isMemberDirectlyAccessible(ctor.getModifiers(), declaringClass, accessingClass); } - return AsmClassGenerator.isMemberDirectlyAccessible(ctor.getModifiers(), declaringClass, accessingClass); + // a protected or package-private constructor needs the same run-time package, i.e. the same package + // name and class loader; a class of another compilation unit may come from another class loader + // (only super(...) can call a protected constructor of another package) + return declaringClass.isPrimaryClassNode() && samePackageName(declaringClass, accessingClass); } private MethodNode addSyntheticMethod(final String methodName, final ClassNode returnType, final Expression returnValue, final Parameter[] parameters, final ClassNode[] exceptions) { diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index dd6e40b7169..568b8118960 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -1430,6 +1430,51 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 + @Test // class::new + void testFunctionCN22() { + assertScript shell, '''package p + class A { + final String value + protected A(String value) { this.value = 'prot:' + value } + @PackageScope A(Integer i) { this.value = 'pkg:' + i } + } + + @CompileStatic + class C { + static Function prot() { A::new } + static Function pkg() { A::new } + } + + assert C.prot().apply('x').value == 'prot:x' + assert C.pkg().apply(1).value == 'pkg:1' + ''' + } + + // GROOVY-12488 + @Test // class::new + void testFunctionCN23() { + assertScript shell, '''package p + class A { + final String value + protected A(String value) { this.value = 'prot:' + value } + @PackageScope A(Integer i) { this.value = 'pkg:' + i } + } + + true + ''' + assertScript shell, '''package p + @CompileStatic + class C { // same package name as A, but another class loader + static Function prot() { A::new } + static Function pkg() { A::new } + } + + assert C.prot().apply('x').value == 'prot:x' + assert C.pkg().apply(1).value == 'pkg:1' + ''' + } + @Test // arrayClass::new void testIntFunctionCN() { assertScript shell, ''' @@ -3239,6 +3284,65 @@ final class MethodReferenceTest { assert bytecode.hasSequence(['INVOKEDYNAMIC init(Ljava/lang/Class;)Ljava/lang/Object;']) } + // GROOVY-12488 + @Test + void testProtectedAndPackagePrivateConstructorsOfSamePackageAreCalledDirectly() { + def script = ''' + class A { + protected A(String s) { } + @groovy.transform.PackageScope A(Integer i) { } + } + @CompileStatic + class C { + static Function prot() { + A::new + } + static Function pkg() { + A::new + } + } + ''' + + def prot = compileStaticMethodBytecode('ctorRef$prot$0', script) + assert prot.hasSequence(['NEW A', 'ALOAD 0', 'INVOKESPECIAL A. (Ljava/lang/String;)V']) + assertNoDynamicConstructorCall(prot) + + def pkg = compileStaticMethodBytecode('ctorRef$pkg$1', script) + assert pkg.hasSequence(['NEW A', 'ALOAD 0', 'INVOKESPECIAL A. (Ljava/lang/Integer;)V']) + assertNoDynamicConstructorCall(pkg) + } + + // GROOVY-12488 + @Test + void testProtectedAndPackagePrivateConstructorsOfClassCompiledBeforeAreSelectedAtRuntime() { + def library = '''package p + class A { + protected A(String s) { } + @groovy.transform.PackageScope A(Integer i) { } + } + ''' + def script = '''package p + import java.util.function.Function + @groovy.transform.CompileStatic + class C { + static Function prot() { + A::new + } + static Function pkg() { + A::new + } + } + ''' + + def prot = compileStaticMethodBytecodeAgainstLibrary(library, 'p.C', 'ctorRef$prot$0', script) + assert prot.hasSequence(['INVOKEDYNAMIC init(']) + assert !prot.hasSequence(['INVOKESPECIAL p/A.']) + + def pkg = compileStaticMethodBytecodeAgainstLibrary(library, 'p.C', 'ctorRef$pkg$1', script) + assert pkg.hasSequence(['INVOKEDYNAMIC init(']) + assert !pkg.hasSequence(['INVOKESPECIAL p/A.']) + } + @Test void testArrayConstructorReferenceUsesSyntheticArrayFactoryMethod() { def script = ''' From 9b7c30141d1745e894e48ea9a5108c670d3f30bc Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Sat, 24 Oct 2026 18:12:15 -0700 Subject: [PATCH 04/15] GROOVY-12488: do not make the synthetic method of a method reference final in an interface The synthetic method that implements a constructor reference, or adapts a variadic or extension method reference, is private, static and final. The JVM rejects a final private method in an interface, so a method reference that needs one, such as Foo::new or Arrays::asList as a Supplier, in a static or default method of a statically compiled interface failed with ClassFormatError when the interface was loaded. The method is no longer final in an interface; it is private, so it cannot be overridden anyway. --- ...cTypesMethodReferenceExpressionWriter.java | 6 +++-- .../transform/stc/MethodReferenceTest.groovy | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java index aba8bbe1b82..831924c540b 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java @@ -522,9 +522,11 @@ private boolean isDirectlyAccessible(final ConstructorNode ctor) { } private MethodNode addSyntheticMethod(final String methodName, final ClassNode returnType, final Expression returnValue, final Parameter[] parameters, final ClassNode[] exceptions) { - return controller.getClassNode().addSyntheticMethod( + ClassNode classNode = controller.getClassNode(); + return classNode.addSyntheticMethod( methodName, - Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, + // the JVM rejects a private method of an interface that is final + Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | (classNode.isInterface() ? 0 : Opcodes.ACC_FINAL), returnType, parameters, exceptions, diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index 568b8118960..096cba19aa4 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -1475,6 +1475,31 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 + @Test + void testMethodReferencesInStaticAndDefaultMethodsOfInterface() { + assertScript shell, ''' + class Foo { + final String value + Foo(String value) { this.value = value } + } + + @CompileStatic + interface I { + static Function make() { Foo::new } + default Function create() { Foo::new } + static Supplier list() { Arrays::asList } + default String join(String... parts) { parts.join(',') } + default BiFunction joiner() { this::join } + } + + assert I.make().apply('s').value == 's' + assert new I() {}.create().apply('d').value == 'd' + assert I.list().get() == [] + assert new I() {}.joiner().apply('a', 'b') == 'a,b' + ''' + } + @Test // arrayClass::new void testIntFunctionCN() { assertScript shell, ''' From 156f1615a2e7870418054a19be2fde8bb6f15907 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Sat, 24 Oct 2026 18:17:03 -0700 Subject: [PATCH 05/15] GROOVY-12488: test that a constructor reference does not go through the meta-class A statically compiled constructor reference now calls the constructor directly, as new Foo(s) does under static compilation, so a constructor added to the meta-class, for example with ExpandoMetaClass, is no longer used by it. Pin this with a test, and mark the tests added for GROOVY-12488 with the issue key. --- .../transform/stc/MethodReferenceTest.groovy | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index 096cba19aa4..c1fb78805b8 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -1151,6 +1151,7 @@ final class MethodReferenceTest { assert err.message =~ /Cannot call java.lang.Integer\[\]#\(int\) with arguments / } + // GROOVY-12488 @Test // class::new void testFunctionCN10() { assertScript shell, ''' @@ -1180,6 +1181,7 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 @Test // class::new void testFunctionCN11() { assertScript shell, ''' @@ -1204,6 +1206,7 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 @Test // class::new void testFunctionCN12() { assertScript shell, ''' @@ -1229,6 +1232,7 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 @Test // class::new void testFunctionCN13() { assertScript shell, ''' @@ -1261,6 +1265,7 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 @Test // class::new void testFunctionCN14() { assertScript shell, ''' @@ -1282,6 +1287,7 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 @Test // class::new void testFunctionCN15() { assertScript shell, ''' @@ -1302,7 +1308,7 @@ final class MethodReferenceTest { ''' } - // GROOVY-10971 + // GROOVY-10971, GROOVY-12488 @Test // class::new void testFunctionCN16() { assertScript shell, ''' @@ -1321,6 +1327,7 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 @Test // class::new void testFunctionCN17() { assertScript shell, '''package p @@ -1344,6 +1351,7 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 @Test // class::new void testFunctionCN18() { assertScript shell, ''' @@ -1475,6 +1483,31 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 + @Test // class::new + void testFunctionCN24() { + assertScript shell, ''' + class Foo { + final String how + Foo(String s) { how = 'real:' + s } + } + + @CompileStatic + String test() { + Function f = Foo::new // calls the constructor directly, like new Foo(s) + f.apply('x').how + } + + Foo.metaClass.constructor = { String s -> Foo.getDeclaredConstructor(String).newInstance('emc-' + s) } + try { + assert new Foo('x').how == 'real:emc-x' + assert test() == 'real:x' + } finally { + GroovySystem.metaClassRegistry.removeMetaClass(Foo) + } + ''' + } + // GROOVY-12488 @Test void testMethodReferencesInStaticAndDefaultMethodsOfInterface() { @@ -3072,6 +3105,7 @@ final class MethodReferenceTest { ) } + // GROOVY-12488 @Test void testConstructorReferenceCallsNoArgConstructorDirectly() { def helperBytecode = compileStaticMethodBytecode('ctorRef$create$0', ''' @@ -3095,6 +3129,7 @@ final class MethodReferenceTest { assertNoDynamicConstructorCall(helperBytecode) } + // GROOVY-12488 @Test void testConstructorReferenceSelectsConstructorBySamParameterTypes() { def script = ''' @@ -3138,6 +3173,7 @@ final class MethodReferenceTest { assertNoDynamicConstructorCall(unboxAndWiden) } + // GROOVY-12488 @Test void testVariadicConstructorReferenceCallsConstructorDirectly() { def script = ''' @@ -3173,6 +3209,7 @@ final class MethodReferenceTest { assertNoDynamicConstructorCall(array) } + // GROOVY-12488 @Test void testGenericClassConstructorReferenceCallsConstructorDirectly() { def bytecode = compileStaticMethodBytecode('ctorRef$create$0', ''' @@ -3191,6 +3228,7 @@ final class MethodReferenceTest { assertNoDynamicConstructorCall(bytecode) } + // GROOVY-12488 @Test void testPrivateConstructorOfNestmateIsCalledDirectly() { def bytecode = compileStaticMethodBytecode('ctorRef$create$0', ''' @@ -3276,7 +3314,7 @@ final class MethodReferenceTest { assertNoDynamicConstructorCall(bytecode) } - // GROOVY-10971: no constructor accepts the parameter type of the functional interface + // GROOVY-10971, GROOVY-12488: no constructor accepts the parameter type of the functional interface @Test void testConstructorReferenceWithoutMatchingConstructorSelectsConstructorAtRuntime() { def bytecode = compileStaticMethodBytecode('ctorRef$create$0', ''' @@ -3294,6 +3332,7 @@ final class MethodReferenceTest { assert bytecode.hasSequence(['INVOKEDYNAMIC init(Ljava/lang/Class;Ljava/lang/Object;)Ljava/lang/Object;']) } + // GROOVY-12488 @Test void testProtectedConstructorOfAnotherPackageIsSelectedAtRuntime() { def bytecode = compileStaticMethodBytecode('ctorRef$create$0', ''' From 0ac6928ef3c915fdac77ea68493c13e345bf63b9 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Sat, 24 Oct 2026 22:17:02 -0700 Subject: [PATCH 06/15] GROOVY-12488: SC: construct an object from a Map variable as type-checked code does new A(map), where map is a Map variable rather than a map literal and A declares only a no-argument constructor, is a map-style construction: Groovy calls the no-argument constructor and sets a property for each entry of the map. The type checker accepts it: as the property names are only known at run time, it does not select a constructor, and type-checked code constructs the object through the meta-class. Under static compilation nothing compiled such a call, and compilation stopped with the internal error "Target constructor for constructor call expression hasn't been set". A constructor reference such as Function f = A::new stopped with the same error, because the type checker checks it as new A(map). The type checker now marks such a call for dynamic resolution, and the static compiler creates the object through the meta-class, as type-checked and dynamic code do. A map literal is still compiled to the no-argument constructor and a setter call for each entry, and a constructor reference to such a class still selects the constructor at run time. (Test only: GROOVY-12454 on master already makes static compilation construct such an object through the meta-class, so only the test of the constructor reference, testFunctionCN25, is kept.) --- .../transform/stc/MethodReferenceTest.groovy | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index c1fb78805b8..db47bfe0379 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -1508,6 +1508,24 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 + @Test // class::new + void testFunctionCN25() { + assertScript shell, ''' + class A { + String name + } + + @CompileStatic + List test(Map map) { + Function f = A::new // map-style construction, like new A(map) + [f.apply(map).name, new A(map).name] + } + + assert test([name: 'x']) == ['x', 'x'] + ''' + } + // GROOVY-12488 @Test void testMethodReferencesInStaticAndDefaultMethodsOfInterface() { From 352bb4a68220417a1a9b202c159249a9fcc6a1df Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Sat, 24 Oct 2026 23:06:20 -0700 Subject: [PATCH 07/15] GROOVY-12488: use non-public members of another compilation unit through the meta-class only for classes defined in memory The previous commits accessed a protected or package-private member of a class with the same package name through the meta-class whenever that class was compiled by another compilation unit. That also hit class files: a Java class of a joint compilation, a main class used by a test, a class left over from an incremental compilation. Such classes are loaded by one class loader at run time, as javac assumes, so the direct access worked; the meta-class path was slower, followed meta-class semantics, needed reflection metadata in a native image, and made an incremental build give other bytecode than a full one. The run-time package of such a class only differs when the classes are defined in memory by a GroovyClassLoader, which defines each compilation (a GroovyShell script, parseClass, JSR-223) with an InnerLoader of its own. GroovyClassLoader now records that on the compilation unit, and AsmClassGenerator.isInSameRuntimePackage treats a class of another compilation unit as another run-time package only then. Constructors, map-style construction, methods, getters and setters, method references, fields, dynamic super and this.@ access and super$ methods all use this check. Compiling to class files keeps direct access, as before these changes and as javac does. (Constructor part: the GroovyClassLoader marker, AsmClassGenerator.isInSameRuntimePackage and StaticInvocationWriter.isDirectlyAccessible, which constructor references use. The same rule for new, methods, fields, dynamic super, this.@ and super$ methods follows separately.) --- .../java/groovy/lang/GroovyClassLoader.java | 11 +++ .../groovy/classgen/AsmClassGenerator.java | 32 +++++++++ .../asm/sc/StaticInvocationWriter.java | 18 +++++ ...cTypesMethodReferenceExpressionWriter.java | 19 +----- .../transform/stc/MethodReferenceTest.groovy | 68 +++++++++++++++++-- 5 files changed, 126 insertions(+), 22 deletions(-) diff --git a/src/main/java/groovy/lang/GroovyClassLoader.java b/src/main/java/groovy/lang/GroovyClassLoader.java index a382c783108..c44dd5f6734 100644 --- a/src/main/java/groovy/lang/GroovyClassLoader.java +++ b/src/main/java/groovy/lang/GroovyClassLoader.java @@ -237,6 +237,7 @@ public Class defineClass(final ClassNode classNode, final String file, final Str } CompilationUnit unit = createCompilationUnit(config, codeSource); + markDefinedInMemory(unit); var generatedClasses = new LinkedHashMap(); ClassCollector collector = createCollector(unit, classNode.getModule().getContext()); try { @@ -414,6 +415,7 @@ private Class doParseClass(final GroovyCodeSource codeSource) { validate(codeSource); // Was neither already loaded nor compiling, so compile and add to cache. CompilationUnit unit = createCompilationUnit(config, codeSource.getCodeSource()); + markDefinedInMemory(unit); if (recompile != null ? recompile : config.getRecompileGroovySource()) { unit.addFirstPhaseOperation(TimestampAdder.INSTANCE, CompilePhase.CLASS_GENERATION.getPhaseNumber()); } @@ -448,6 +450,15 @@ private Class doParseClass(final GroovyCodeSource codeSource) { return answer; } + /** + * Records that the classes of the compilation unit are defined by an + * {@link InnerLoader} of their own, so that the compiler does not assume + * that a class of another compilation unit is in the same run-time package. + */ + private static void markDefinedInMemory(final CompilationUnit unit) { + unit.getAST().putNodeMetaData(InnerLoader.class, Boolean.TRUE); + } + private static void validate(final GroovyCodeSource codeSource) { if (codeSource.getFile() == null) { if (codeSource.getScriptText() == null) { diff --git a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java index ba6c0eb06c3..b1bb2a9927a 100644 --- a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java +++ b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java @@ -18,6 +18,7 @@ */ package org.codehaus.groovy.classgen; +import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyRuntimeException; import org.apache.groovy.ast.tools.ExpressionUtils; import org.apache.groovy.io.StringBuilderWriter; @@ -1312,6 +1313,37 @@ public static boolean isFieldDirectlyAccessible(final FieldNode field, final Cla return field != null && isMemberDirectlyAccessible(field.getModifiers(), field.getDeclaringClass(), accessingClass); } + /** + * Determines if the bytecode of the accessing class can rely on being in + * the run-time package of the declaring class, which is the package name + * and the class loader. The JVM allows access to a protected or + * package-private member only within the same run-time package, apart from + * the subclass rule for a protected member. + *

+ * A GroovyClassLoader defines the classes of each compilation, like a + * {@code GroovyShell} script, in memory with a class loader of their own, + * so a class compiled before, by another compilation, is in another + * run-time package although its package name is the same. Classes that + * are compiled to class files are expected to be loaded by one class + * loader with the classes they use from the same package, as javac + * expects. + * + * @param declaringClass the class declaring the member + * @param accessingClass the class attempting access + * @return {@code true} if both classes have the same package name and are + * not known to be defined by different class loaders + */ + public static boolean isInSameRuntimePackage(final ClassNode declaringClass, final ClassNode accessingClass) { + if (!Objects.equals(declaringClass.getPackageName(), accessingClass.getPackageName())) return false; + if (declaringClass.isPrimaryClassNode()) return true; // compiled and defined with the accessing class + CompileUnit unit = null; + for (ClassNode cn = accessingClass; unit == null && cn != null; cn = cn.getOuterClass()) { + unit = cn.getCompileUnit(); + } + // see GroovyClassLoader: the classes of this compilation are defined by an InnerLoader of their own + return unit == null || !Boolean.TRUE.equals(unit.getNodeMetaData(GroovyClassLoader.InnerLoader.class)); + } + /** * Determines whether a member with the supplied modifiers is directly accessible to a class. * diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticInvocationWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticInvocationWriter.java index 11a80c7754d..8c703ff60f5 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticInvocationWriter.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticInvocationWriter.java @@ -175,6 +175,24 @@ public void writeInvokeConstructor(final ConstructorCallExpression call) { finnishConstructorCall(cn, ownerDescriptor, controller.getOperandStack().getStackLength() - before); } + /** + * Determines if the bytecode of the accessing class can call the constructor + * with {@code NEW} and {@code INVOKESPECIAL}. + *

+ * A protected or package-private constructor needs the same run-time package + * (see {@link AsmClassGenerator#isInSameRuntimePackage}). Outside its package, + * a protected constructor can only be called by {@code super(...)} or an + * anonymous inner class. + */ + static boolean isDirectlyAccessible(final ConstructorNode ctor, final ClassNode accessingClass) { + if (ctor.isPublic()) return true; + ClassNode declaringClass = ctor.getDeclaringClass(); + if (ctor.isPrivate()) { // nestmates can call a private constructor + return AsmClassGenerator.isMemberDirectlyAccessible(ctor.getModifiers(), declaringClass, accessingClass); + } + return AsmClassGenerator.isInSameRuntimePackage(declaringClass, accessingClass); + } + /** {@inheritDoc} */ @Override public void writeSpecialConstructorCall(final ConstructorCallExpression call) { diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java index 831924c540b..184c8d933cb 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java @@ -55,7 +55,6 @@ import static java.util.Comparator.comparingInt; import static java.util.stream.Collectors.joining; -import static org.apache.groovy.ast.tools.ClassNodeUtils.samePackageName; import static org.codehaus.groovy.ast.tools.GeneralUtils.callX; import static org.codehaus.groovy.ast.tools.GeneralUtils.classX; import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; @@ -500,27 +499,13 @@ private ConstructorNode findConstructorReferenceTarget(final ClassNode type, fin // in a closure or lambda, visibility is checked for the enclosing class, as the type checker does List visible = filterMethodsByVisibility(new ArrayList<>(type.getDeclaredConstructors()), controller.getThisType()); List ctors = chooseBestMethod(type, visible, argumentTypes); - if (ctors.size() == 1 && ctors.get(0) instanceof ConstructorNode ctor && isDirectlyAccessible(ctor)) { + if (ctors.size() == 1 && ctors.get(0) instanceof ConstructorNode ctor + && StaticInvocationWriter.isDirectlyAccessible(ctor, controller.getClassNode())) { return ctor; } return null; } - /** - * Determines if the bytecode of the current class can call the constructor. - */ - private boolean isDirectlyAccessible(final ConstructorNode ctor) { - if (ctor.isPublic()) return true; - ClassNode declaringClass = ctor.getDeclaringClass(), accessingClass = controller.getClassNode(); - if (ctor.isPrivate()) { // nestmates can call a private constructor - return AsmClassGenerator.isMemberDirectlyAccessible(ctor.getModifiers(), declaringClass, accessingClass); - } - // a protected or package-private constructor needs the same run-time package, i.e. the same package - // name and class loader; a class of another compilation unit may come from another class loader - // (only super(...) can call a protected constructor of another package) - return declaringClass.isPrimaryClassNode() && samePackageName(declaringClass, accessingClass); - } - private MethodNode addSyntheticMethod(final String methodName, final ClassNode returnType, final Expression returnValue, final Parameter[] parameters, final ClassNode[] exceptions) { ClassNode classNode = controller.getClassNode(); return classNode.addSyntheticMethod( diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index db47bfe0379..f2d0bd37125 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -21,12 +21,14 @@ package groovy.transform.stc import org.codehaus.groovy.classgen.asm.AbstractBytecodeTestCase import org.codehaus.groovy.classgen.asm.InstructionSequence import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.CompilerConfiguration import org.codehaus.groovy.control.Phases import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource +import java.security.CodeSource import java.util.regex.Pattern import static groovy.test.GroovyAssert.assertScript @@ -3425,6 +3427,46 @@ final class MethodReferenceTest { assert !pkg.hasSequence(['INVOKESPECIAL p/A.']) } + // GROOVY-12488 + @Test + void testProtectedAndPackagePrivateConstructorsOfClassCompiledBeforeToClassFilesAreCalledDirectly() { + def dir = File.createTempDir() + try { + compileToDirectory(dir, 'A.groovy': '''package p + class A { + final String value + protected A(String s) { value = 'prot:' + s } + @groovy.transform.PackageScope A(Integer i) { value = 'pkg:' + i } + } + ''') + compileToDirectory(dir, 'C.groovy': '''package p + import java.util.function.Function + @groovy.transform.CompileStatic + class C { // compiled by another compilation unit, to class files like groovyc + static Function prot() { + A::new + } + static Function pkg() { + A::new + } + } + ''') + + byte[] bytes = new File(dir, 'p/C.class').bytes + def prot = selectMethod('ctorRef$prot$0', extractSequence(bytes, [method: 'ctorRef$prot$0'])) + assert prot.hasSequence(['NEW p/A', 'DUP', 'ALOAD 0', 'INVOKESPECIAL p/A. (Ljava/lang/String;)V']) + def pkg = selectMethod('ctorRef$pkg$1', extractSequence(bytes, [method: 'ctorRef$pkg$1'])) + assert pkg.hasSequence(['NEW p/A', 'DUP', 'ALOAD 0', 'INVOKESPECIAL p/A. (Ljava/lang/Integer;)V']) + + // one class loader for all the class files, as javac expects + def c = new URLClassLoader([dir.toURI().toURL()] as URL[], this.class.classLoader).loadClass('p.C') + assert c.prot().apply('x').value == 'prot:x' + assert c.pkg().apply(1).value == 'pkg:1' + } finally { + dir.deleteDir() + } + } + @Test void testArrayConstructorReferenceUsesSyntheticArrayFactoryMethod() { def script = ''' @@ -3526,19 +3568,35 @@ final class MethodReferenceTest { /** * Compiles {@code script} against the classes of {@code library}, which - * are compiled before, by another compilation unit and class loader. + * are compiled before, by another parse of the same GroovyClassLoader, + * which defines them with another class loader. */ private InstructionSequence compileStaticMethodBytecodeAgainstLibrary(final String library, final String className, final String method, final String script) { - def loader = new GroovyClassLoader() + CompilationUnit unit = null + def loader = new GroovyClassLoader() { + @Override + protected CompilationUnit createCompilationUnit(final CompilerConfiguration config, final CodeSource source) { + unit = super.createCompilationUnit(config, source) + } + } loader.parseClass(library) - def unit = new CompilationUnit(null, null, loader) - unit.addSource('script', script) - unit.compile(Phases.CLASS_GENERATION) + loader.parseClass(script) def generated = unit.classes.find { it.name == className } assert generated != null : "class $className not found" selectMethod(method, extractSequence(generated.bytes, [method: method])) } + /** + * Compiles the sources to class files in {@code dir}, against the class + * files already there, as groovyc does. + */ + private static void compileToDirectory(final Map sources, final File dir) { + def config = new CompilerConfiguration(targetDirectory: dir, classpathList: [dir.path]) + def unit = new CompilationUnit(config, null, new GroovyClassLoader(MethodReferenceTest.classLoader, config)) + sources.each { name, text -> unit.addSource(name, text) } + unit.compile(Phases.OUTPUT) + } + private static InstructionSequence selectMethod(final String method, final InstructionSequence sequence) { List instructions = sequence.instructions int begin = instructions.indexOf('--BEGIN--'), end = instructions.indexOf('--END--') From 244846908c8c3a62c2b181ef426ad367f85942f8 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Sun, 25 Oct 2026 09:15:36 -0700 Subject: [PATCH 08/15] GROOVY-12488: statically compile the synthetic method of an array constructor reference The synthetic method of an array constructor reference such as String[]::new was the one constructor-reference method still marked as not statically compiled. It is now compiled by the static writers, like the method of a class constructor reference that has a target constructor. Its bytecode does not change: the array was already created with NEWARRAY, ANEWARRAY or MULTIANEWARRAY, and the size unboxed the way a statically compiled new T[n] does (Integer.intValue, or DefaultTypeTransformation.intUnbox for a Short or Byte). The new tests pin that bytecode, compared with new String[s], and the behavior for each size type the type checker accepts (int, short, byte and their wrappers), multi-dimensional and primitive arrays, closures and lambdas, a negative size and null. --- ...cTypesMethodReferenceExpressionWriter.java | 3 +- .../transform/stc/MethodReferenceTest.groovy | 91 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java index 184c8d933cb..d0370eb3398 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java @@ -463,10 +463,11 @@ private MethodNode addSyntheticMethodForConstructorReference(final String method Expression returnValue; boolean staticCompile = false; - if (returnType.isArray()) { + if (returnType.isArray()) { // like new T[n] returnValue = new ArrayExpression( returnType.getComponentType(), null, ctorArgs.getExpressions()); + staticCompile = true; } else { returnValue = ctorX(returnType, ctorArgs); ConstructorNode ctor = findConstructorReferenceTarget(returnType, parametersWithExactType); diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index f2d0bd37125..11a0770cc0b 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -1567,6 +1567,48 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 + @Test // arrayClass::new + void testArrayConstructorReferencesWithEachSizeType() { + assertScript shell, ''' + interface ShortFunction { T apply(short s) } + interface ByteFunction { T apply(byte b) } + + @CompileStatic + void test() { + IntFunction i = String[]::new + Function boxed = String[]::new + Function s = String[]::new + Function b = String[]::new + ShortFunction ps = long[]::new + ByteFunction pb = Object[]::new + IntFunction m = int[][]::new + IntFunction[]> g = List[]::new + + assert i.apply(1) == new String[1] + assert boxed.apply(2) == new String[2] + assert s.apply((short) 3) == new String[3] + assert b.apply((byte) 4) == new String[4] + assert ps.apply((short) 2) == new long[] { 0L, 0L } + assert pb.apply((byte) 1).getClass() == Object[] + int[][] matrix = m.apply(3) + assert matrix.length == 3 && matrix[0] == null + assert g.apply(1).getClass() == List[] + + def inClosure = { -> IntFunction f = String[]::new; f } + Supplier> inLambda = () -> { IntFunction f = String[]::new; f } + assert inClosure().apply(1).length == 1 + assert inLambda.get().apply(2).length == 2 + + try { i.apply(-1); assert false } catch (NegativeArraySizeException expected) { } + try { boxed.apply(null); assert false } catch (NullPointerException expected) { } + try { s.apply(null); assert false } catch (org.codehaus.groovy.runtime.typehandling.GroovyCastException expected) { } + } + + test() + ''' + } + @Test // class::new void testSerializableConstructorReference() { assertScript shell, ''' @@ -3494,6 +3536,55 @@ final class MethodReferenceTest { ) } + // GROOVY-12488 + @Test + void testArrayConstructorReferenceCreatesArrayLikeStaticallyCompiledNewArray() { + def script = ''' + @CompileStatic + class C { + static IntFunction ints() { + String[]::new + } + static Function boxed() { + String[]::new + } + static Function shorts() { + String[]::new + } + static IntFunction primitive() { + long[]::new + } + static IntFunction matrix() { + int[][]::new + } + static String[] newArray(Short s) { + new String[s] + } + } + ''' + + def ints = compileStaticMethodBytecode('ctorRef$ints$0', script) + assert ints.hasStrictSequence(['ILOAD 0', 'ANEWARRAY java/lang/String', 'ARETURN']) + + def boxed = compileStaticMethodBytecode('ctorRef$boxed$1', script) + assert boxed.hasStrictSequence(['ALOAD 0', 'INVOKEVIRTUAL java/lang/Integer.intValue ()I', 'ANEWARRAY java/lang/String', 'ARETURN']) + + def shortSize = ['ALOAD 0', 'INVOKESTATIC org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.intUnbox (Ljava/lang/Object;)I', 'ANEWARRAY java/lang/String', 'ARETURN'] + def shorts = compileStaticMethodBytecode('ctorRef$shorts$2', script) + assert shorts.hasStrictSequence(shortSize) + assert compileStaticMethodBytecode('newArray', script).hasStrictSequence(shortSize) + + def primitive = compileStaticMethodBytecode('ctorRef$primitive$3', script) + assert primitive.hasStrictSequence(['ILOAD 0', 'NEWARRAY T_LONG', 'ARETURN']) + + def matrix = compileStaticMethodBytecode('ctorRef$matrix$4', script) + assert matrix.hasStrictSequence(['ILOAD 0', 'MULTIANEWARRAY [[I 1', 'ARETURN']) + + for (bytecode in [ints, boxed, shorts, primitive, matrix]) { + assert !bytecode.hasSequence(['INVOKEDYNAMIC']) + } + } + @Test void testSerializableMethodReferenceUsesAltMetafactoryAndDeserializeDispatcher() { def script = ''' From 0b6ae282babfc11c9bbc3e7354fa3613e7fb10ce Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Sun, 25 Oct 2026 09:16:36 -0700 Subject: [PATCH 09/15] GROOVY-12488: document the run-time package, subclass and meta-class rules of constructor references - isInSameRuntimePackage: isPrimaryClassNode() stands for "defined by the class loader of the accessing class", not for "compiled from source". - StaticInvocationWriter.isDirectlyAccessible: unlike isMemberDirectlyAccessible it has no subclass rule, since the verifier rejects NEW and INVOKESPECIAL of a protected constructor of another package in a subclass too. - findConstructorReferenceTarget: a selected constructor is called directly, so a meta-class constructor override is not used, as for a statically compiled new. The guard comment now says why enums are left to the meta-class: the type checker accepts an enum constructor reference whose arguments match the generated constructor, but Java code cannot instantiate an enum. - addSyntheticMethod: the ACC_FINAL rule of JVMS 4.6 applies to every helper method of a method reference in an interface. (Constructor part: the notes on statically compiled new follow separately.) --- .../codehaus/groovy/classgen/AsmClassGenerator.java | 4 ++++ .../groovy/classgen/asm/sc/StaticInvocationWriter.java | 5 +++++ .../sc/StaticTypesMethodReferenceExpressionWriter.java | 10 ++++++++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java index b1bb2a9927a..51595a53815 100644 --- a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java +++ b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java @@ -1327,6 +1327,10 @@ public static boolean isFieldDirectlyAccessible(final FieldNode field, final Cla * are compiled to class files are expected to be loaded by one class * loader with the classes they use from the same package, as javac * expects. + *

+ * {@code isPrimaryClassNode()} is used here as "defined by the class + * loader of the accessing class", since the classes of one compilation are + * defined together; it does not stand for "compiled from source". * * @param declaringClass the class declaring the member * @param accessingClass the class attempting access diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticInvocationWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticInvocationWriter.java index 8c703ff60f5..1211ab7c56d 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticInvocationWriter.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticInvocationWriter.java @@ -183,6 +183,11 @@ public void writeInvokeConstructor(final ConstructorCallExpression call) { * (see {@link AsmClassGenerator#isInSameRuntimePackage}). Outside its package, * a protected constructor can only be called by {@code super(...)} or an * anonymous inner class. + *

+ * Unlike {@link AsmClassGenerator#isMemberDirectlyAccessible}, there is no + * subclass rule: the verifier rejects {@code NEW} and {@code INVOKESPECIAL} + * of a protected constructor of another package in a subclass too, so a + * constructor reference to it creates the object through the meta-class. */ static boolean isDirectlyAccessible(final ConstructorNode ctor, final ClassNode accessingClass) { if (ctor.isPublic()) return true; diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java index d0370eb3398..32f90166669 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesMethodReferenceExpressionWriter.java @@ -488,12 +488,18 @@ private MethodNode addSyntheticMethodForConstructorReference(final String method * of the functional interface, among the constructors visible from the * enclosing class, as the type checker selects the constructor of a * constructor call. + *

+ * A returned constructor is called directly, like the constructor of a + * statically compiled {@code new}, so the meta-class does not see the call: + * a constructor added or overridden through the meta-class, for example + * with {@code ExpandoMetaClass}, is not used. * * @return the constructor, or {@code null} if no single constructor matches * or the matching constructor cannot be called directly */ private ConstructorNode findConstructorReferenceTarget(final ClassNode type, final Parameter[] samParameters) { - // the type checker rejects these, unless a type checking extension suppresses the error + // the type checker rejects an abstract class or interface, unless a type checking extension suppresses the error; + // an enum, which Java code cannot instantiate at all, is left to the meta-class if (type.isInterface() || type.isAbstract() || type.isEnum()) return null; ClassNode[] argumentTypes = Arrays.stream(samParameters).map(Parameter::getType).toArray(ClassNode[]::new); @@ -511,7 +517,7 @@ private MethodNode addSyntheticMethod(final String methodName, final ClassNode r ClassNode classNode = controller.getClassNode(); return classNode.addSyntheticMethod( methodName, - // the JVM rejects a private method of an interface that is final + // the JVM rejects ACC_FINAL on any method of an interface (JVMS 4.6), so no helper method is final there Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | (classNode.isInterface() ? 0 : Opcodes.ACC_FINAL), returnType, parameters, From 7c55016559131faa0d31a37e8df29373f3c27fc7 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Sun, 25 Oct 2026 09:20:39 -0700 Subject: [PATCH 10/15] GROOVY-12488: test constructor references to default-argument, enum and abstract-class constructors Default arguments: the constructors that Verifier generates for default arguments keep the modifiers of the declared constructor and are not synthetic, and every class of a compilation is verified before any class is generated, so a constructor reference whose functional interface matches a generated constructor calls it directly, also when the class is declared after the reference, when the constructor is package-private, when every parameter has a default, and for a public constructor of a class compiled by an earlier parse. A package-private one of such a class is still selected at run time. Enums: the type checker accepts E::new inside the enum when the functional interface matches the constructor Groovy generates (name, ordinal, ...); the reference stays on the run-time path, while javac rejects it outright. Abstract classes: A::new is a type checking error also inside an anonymous subclass. A protected constructor of another package referenced from an anonymous subclass is selected at run time and works. --- .../transform/stc/MethodReferenceTest.groovy | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index 11a0770cc0b..ea9f29add32 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -1528,6 +1528,86 @@ final class MethodReferenceTest { ''' } + // GROOVY-12488 + @Test // class::new + void testFunctionCN26() { + assertScript shell, ''' + class Lib { + final String value + Lib(String a, String b = 'b') { value = a + b } + } + + true + ''' + assertScript shell, ''' + @CompileStatic + class C { + static List test() { + Function one = Dflt::new // the constructor generated for the default argument + Supplier none = All::new + Function pkg = Pkg::new + Function lib = Lib::new // compiled by the parse before + [one.apply('a').value, none.get().value, pkg.apply('p').value, lib.apply('l').value] + } + } + class Dflt { // declared after C + final String value + Dflt(String a, String b = 'b') { value = a + b } + } + class All { + final String value + All(String a = 'a', String b = 'b') { value = a + b } + } + class Pkg { + final String value + @PackageScope Pkg(String a, Integer i = 1) { value = a + i } + } + + assert C.test() == ['ab', 'ab', 'p1', 'lb'] + ''' + } + + // GROOVY-11385, GROOVY-12488 + @Test // class::new + void testFunctionCN27() { + assertScript shell, '''package p + abstract class A { + final String value + protected A(String value) { this.value = value } + } + class B { + final String value + protected B(String value) { this.value = value } + } + + true + ''' + def err = shouldFail shell, ''' + @CompileStatic + def test() { + def a = new p.A('x') { + Function make() { p.A::new } + } + a.make() + } + + test() + ''' + assert err.message =~ /Cannot instantiate the type p.A/ + + assertScript shell, ''' + @CompileStatic + String test() { + def b = new p.B('x') { + Function make() { p.B::new } // protected constructor of another package + } + b.make().apply('y').value + } + + assert test() == 'y' + ''' + } + // GROOVY-12488 @Test void testMethodReferencesInStaticAndDefaultMethodsOfInterface() { @@ -3509,6 +3589,123 @@ final class MethodReferenceTest { } } + // GROOVY-12488 + @Test + void testConstructorReferenceCallsConstructorGeneratedForDefaultArgumentsDirectly() { + def script = ''' + @CompileStatic + class C { + static Function one() { + Dflt::new + } + static Supplier none() { + All::new + } + static Function pkg() { + Pkg::new + } + } + class Dflt { // declared after C: every class of the compilation is verified before any is generated + Dflt(String a, String b = 'b') { } + } + class All { + All(String a = 'a', String b = 'b') { } + } + class Pkg { + @groovy.transform.PackageScope Pkg(String a, Integer i = 1) { } + } + ''' + + def one = compileStaticMethodBytecode('ctorRef$one$0', script) + assert one.hasStrictSequence(['NEW Dflt', 'DUP', 'ALOAD 0', 'INVOKESPECIAL Dflt. (Ljava/lang/String;)V', 'ARETURN']) + assertNoDynamicConstructorCall(one) + + def none = compileStaticMethodBytecode('ctorRef$none$1', script) + assert none.hasStrictSequence(['NEW All', 'DUP', 'INVOKESPECIAL All. ()V', 'ARETURN']) + assertNoDynamicConstructorCall(none) + + def pkg = compileStaticMethodBytecode('ctorRef$pkg$2', script) + assert pkg.hasStrictSequence(['NEW Pkg', 'DUP', 'ALOAD 0', 'INVOKESPECIAL Pkg. (Ljava/lang/String;)V', 'ARETURN']) + assertNoDynamicConstructorCall(pkg) + } + + // GROOVY-12488 + @Test + void testConstructorReferenceToConstructorGeneratedForDefaultArgumentsOfClassCompiledBefore() { + def library = '''package p + class D { + D(String a, String b = 'b') { } + @groovy.transform.PackageScope D(Integer i, Integer j = 1) { } + } + ''' + def script = '''package p + import java.util.function.Function + @groovy.transform.CompileStatic + class C { + static Function pub() { + D::new + } + static Function pkg() { + D::new + } + } + ''' + + def pub = compileStaticMethodBytecodeAgainstLibrary(library, 'p.C', 'ctorRef$pub$0', script) + assert pub.hasSequence(['NEW p/D', 'ALOAD 0', 'INVOKESPECIAL p/D. (Ljava/lang/String;)V']) + assertNoDynamicConstructorCall(pub) + + def pkg = compileStaticMethodBytecodeAgainstLibrary(library, 'p.C', 'ctorRef$pkg$1', script) + assert pkg.hasSequence(['INVOKEDYNAMIC init(']) + assert !pkg.hasSequence(['INVOKESPECIAL p/D.']) + } + + // GROOVY-12488 + @Test + void testEnumConstructorReferenceSelectsConstructorAtRuntime() { + def bytecode = compileStaticMethodBytecode('ctorRef$create$0', ''' + interface Make { + E make(String name, int ordinal, String s) + } + @CompileStatic + enum E { + A('a') + E(String s) { } + static Make create() { + E::new // matches the private constructor E(String,int,String) that Groovy generates + } + } + ''', 'E') + + assert bytecode.hasSequence(['INVOKEDYNAMIC init(Ljava/lang/Class;Ljava/lang/String;ILjava/lang/String;)Ljava/lang/Object;']) + assert !bytecode.hasSequence(['INVOKESPECIAL E.']) + } + + // GROOVY-12488 + @Test + void testProtectedConstructorOfAnotherPackageIsSelectedAtRuntimeFromAnonymousSubclass() { + def bytecode = compileStaticMethodBytecodeAgainstLibrary('''package p + class B { + protected B(String value) { } + } + ''', 'C$1', 'ctorRef$make$0', ''' + import java.util.function.Function + @groovy.transform.CompileStatic + class C { + static Object create() { + new p.B('x') { + Function make() { + p.B::new + } + } + } + } + ''') + + assert bytecode.hasSequence(['INVOKEDYNAMIC init(']) + assert !bytecode.hasSequence(['INVOKESPECIAL p/B.']) + } + @Test void testArrayConstructorReferenceUsesSyntheticArrayFactoryMethod() { def script = ''' From d797d91607e5eb735f7b6f6a90175350d14b3d5d Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Sun, 25 Oct 2026 09:25:07 -0700 Subject: [PATCH 11/15] GROOVY-12488: test every kind of method-reference helper method in an interface All helper methods that a method reference adds to the current class are created by addSyntheticMethod, which leaves out ACC_FINAL in an interface. The interface tests covered the constructor-reference and variadic helpers; they now also cover the array constructor reference, the static extension method helper (Thread::startDaemon), the Class method helper (String::getName) and the helper that calls a package-private method of a class of another class loader (M::spm, M::pm), in static and default methods, and check that every helper is private and not final. With ACC_FINAL put back, both tests fail with ClassFormatError (illegal modifiers 0x101A). (Constructor-reference part: the case of a method of another run-time package follows separately.) --- .../transform/stc/MethodReferenceTest.groovy | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index ea9f29add32..90845cfba30 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -1624,12 +1624,26 @@ final class MethodReferenceTest { static Supplier list() { Arrays::asList } default String join(String... parts) { parts.join(',') } default BiFunction joiner() { this::join } + static IntFunction array() { String[]::new } + default IntFunction matrix() { int[][]::new } + static Function daemon() { Thread::startDaemon } // static extension method + default Supplier className() { String::getName } // method of Class } assert I.make().apply('s').value == 's' assert new I() {}.create().apply('d').value == 'd' assert I.list().get() == [] assert new I() {}.joiner().apply('a', 'b') == 'a,b' + assert I.array().apply(2).length == 2 + assert new I() {}.matrix().apply(3).length == 3 + Thread thread = I.daemon().apply { -> } + thread.join() + assert thread.daemon + assert new I() {}.className().get() == 'java.lang.String' + + def helpers = I.declaredMethods.findAll { it.synthetic } + assert helpers*.name.collect { it.substring(0, it.indexOf('$')) }.toSet() == ['ctorRef', 'adapt', 'dgsm', 'class'] as Set + assert helpers.every { java.lang.reflect.Modifier.isPrivate(it.modifiers) && !java.lang.reflect.Modifier.isFinal(it.modifiers) } ''' } From 905552732264d3695f4a893acd1fdbee7ab6498a Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Sun, 25 Oct 2026 09:27:51 -0700 Subject: [PATCH 12/15] GROOVY-12488: share the compile-against-a-library bytecode helper in AbstractBytecodeTestCase MethodReferenceTest and StaticCompileRuntimePackageTest each had a copy of the helper that compiles a script against the classes of an earlier parse of the same GroovyClassLoader, and of the code that keeps only the instructions of one method. AbstractBytecodeTestCase now provides them as compileAgainstLibrary, extractMethodSequence and selectMethod, and both tests use them. Test code only; the tests are unchanged. (Here MethodReferenceTest only; StaticCompileRuntimePackageTest, which comes separately, uses the same helpers.) --- .../transform/stc/MethodReferenceTest.groovy | 44 +++------------ .../asm/AbstractBytecodeTestCase.groovy | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+), 36 deletions(-) diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index 90845cfba30..edf93c279da 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -28,7 +28,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import java.security.CodeSource import java.util.regex.Pattern import static groovy.test.GroovyAssert.assertScript @@ -3451,7 +3450,7 @@ final class MethodReferenceTest { assert bytecode.hasSequence(['NEW Foo', 'ALOAD 0', 'INVOKESPECIAL Foo. (Ljava/lang/Object;)V']) assertNoDynamicConstructorCall(bytecode) - bytecode = compileStaticMethodBytecodeAgainstLibrary('''package p + bytecode = compileAgainstLibrary('''package p class A { @groovy.transform.PackageScope A(String s) { } A(Object o) { } @@ -3554,11 +3553,11 @@ final class MethodReferenceTest { } ''' - def prot = compileStaticMethodBytecodeAgainstLibrary(library, 'p.C', 'ctorRef$prot$0', script) + def prot = compileAgainstLibrary(library, 'p.C', 'ctorRef$prot$0', script) assert prot.hasSequence(['INVOKEDYNAMIC init(']) assert !prot.hasSequence(['INVOKESPECIAL p/A.']) - def pkg = compileStaticMethodBytecodeAgainstLibrary(library, 'p.C', 'ctorRef$pkg$1', script) + def pkg = compileAgainstLibrary(library, 'p.C', 'ctorRef$pkg$1', script) assert pkg.hasSequence(['INVOKEDYNAMIC init(']) assert !pkg.hasSequence(['INVOKESPECIAL p/A.']) } @@ -3589,9 +3588,9 @@ final class MethodReferenceTest { ''') byte[] bytes = new File(dir, 'p/C.class').bytes - def prot = selectMethod('ctorRef$prot$0', extractSequence(bytes, [method: 'ctorRef$prot$0'])) + def prot = extractMethodSequence(bytes, 'ctorRef$prot$0') assert prot.hasSequence(['NEW p/A', 'DUP', 'ALOAD 0', 'INVOKESPECIAL p/A. (Ljava/lang/String;)V']) - def pkg = selectMethod('ctorRef$pkg$1', extractSequence(bytes, [method: 'ctorRef$pkg$1'])) + def pkg = extractMethodSequence(bytes, 'ctorRef$pkg$1') assert pkg.hasSequence(['NEW p/A', 'DUP', 'ALOAD 0', 'INVOKESPECIAL p/A. (Ljava/lang/Integer;)V']) // one class loader for all the class files, as javac expects @@ -3665,11 +3664,11 @@ final class MethodReferenceTest { } ''' - def pub = compileStaticMethodBytecodeAgainstLibrary(library, 'p.C', 'ctorRef$pub$0', script) + def pub = compileAgainstLibrary(library, 'p.C', 'ctorRef$pub$0', script) assert pub.hasSequence(['NEW p/D', 'ALOAD 0', 'INVOKESPECIAL p/D. (Ljava/lang/String;)V']) assertNoDynamicConstructorCall(pub) - def pkg = compileStaticMethodBytecodeAgainstLibrary(library, 'p.C', 'ctorRef$pkg$1', script) + def pkg = compileAgainstLibrary(library, 'p.C', 'ctorRef$pkg$1', script) assert pkg.hasSequence(['INVOKEDYNAMIC init(']) assert !pkg.hasSequence(['INVOKESPECIAL p/D.']) } @@ -3698,7 +3697,7 @@ final class MethodReferenceTest { // GROOVY-12488 @Test void testProtectedConstructorOfAnotherPackageIsSelectedAtRuntimeFromAnonymousSubclass() { - def bytecode = compileStaticMethodBytecodeAgainstLibrary('''package p + def bytecode = compileAgainstLibrary('''package p class B { protected B(String value) { } } @@ -3868,26 +3867,6 @@ final class MethodReferenceTest { selectMethod(method, compileStaticBytecode(classNamePattern: Pattern.quote(className), method: method, script)) } - /** - * Compiles {@code script} against the classes of {@code library}, which - * are compiled before, by another parse of the same GroovyClassLoader, - * which defines them with another class loader. - */ - private InstructionSequence compileStaticMethodBytecodeAgainstLibrary(final String library, final String className, final String method, final String script) { - CompilationUnit unit = null - def loader = new GroovyClassLoader() { - @Override - protected CompilationUnit createCompilationUnit(final CompilerConfiguration config, final CodeSource source) { - unit = super.createCompilationUnit(config, source) - } - } - loader.parseClass(library) - loader.parseClass(script) - def generated = unit.classes.find { it.name == className } - assert generated != null : "class $className not found" - selectMethod(method, extractSequence(generated.bytes, [method: method])) - } - /** * Compiles the sources to class files in {@code dir}, against the class * files already there, as groovyc does. @@ -3899,13 +3878,6 @@ final class MethodReferenceTest { unit.compile(Phases.OUTPUT) } - private static InstructionSequence selectMethod(final String method, final InstructionSequence sequence) { - List instructions = sequence.instructions - int begin = instructions.indexOf('--BEGIN--'), end = instructions.indexOf('--END--') - assert begin >= 0 && end > begin : "method $method not found" - new InstructionSequence(instructions: instructions[(begin + 1).. instructions = sequence.instructions + int begin = instructions.indexOf('--BEGIN--'), end = instructions.indexOf('--END--') + assert begin >= 0 && end > begin : "method $method not found" + new InstructionSequence(instructions: instructions[(begin + 1).. Date: Sun, 25 Oct 2026 11:11:37 -0700 Subject: [PATCH 13/15] GROOVY-12488: ignore the coverage agent's method when listing interface helpers testMethodReferencesInStaticAndDefaultMethodsOfInterface listed every synthetic method of the interface. With coverage on, JaCoCo adds its own synthetic $jacocoInit, so the CI coverage job failed. The test now skips names starting with '$'. --- .../groovy/groovy/transform/stc/MethodReferenceTest.groovy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index edf93c279da..13a8f64386a 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -1640,7 +1640,8 @@ final class MethodReferenceTest { assert thread.daemon assert new I() {}.className().get() == 'java.lang.String' - def helpers = I.declaredMethods.findAll { it.synthetic } + // a coverage agent adds its own synthetic $jacocoInit + def helpers = I.declaredMethods.findAll { it.synthetic && !it.name.startsWith('$') } assert helpers*.name.collect { it.substring(0, it.indexOf('$')) }.toSet() == ['ctorRef', 'adapt', 'dgsm', 'class'] as Set assert helpers.every { java.lang.reflect.Modifier.isPrivate(it.modifiers) && !java.lang.reflect.Modifier.isFinal(it.modifiers) } ''' From 7db442f841f9b56e140b828b1e97ad9948b39de5 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Thu, 29 Oct 2026 11:28:18 -0700 Subject: [PATCH 14/15] GROOVY-12488: document the behavior changes of statically compiled constructor references COMPATIBILITY.md lists the changes for Groovy 7: the overload is chosen by the static parameter types of the functional interface, an Object[] is passed to a variadic constructor as one element, null for a primitive parameter throws NullPointerException, a constructor the enclosing class cannot call no longer wins over one it can, and a meta-class constructor is not used. The method reference section of the operators guide says how the constructor of a constructor reference is selected, when it is still selected at run time, and that those cases need reflection metadata in a native image. --- COMPATIBILITY.md | 35 ++++++++++++++++++++++++++++++++ src/spec/doc/core-operators.adoc | 7 +++++++ 2 files changed, 42 insertions(+) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 09f64fed605..e13bbcd970e 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -739,6 +739,41 @@ which follows `Double.compare` for primitives and objects alike. Classes compiled by an earlier release keep the old comparison until they are recompiled. +### Groovy 7 — statically compiled constructor references call the constructor directly (GROOVY-12488) + +Under `@CompileStatic`, a constructor reference such as `X::new` created the +object through the meta-class, which selected the constructor at run time from +the types of the arguments. The constructor is now selected at compile time, +among the constructors visible from the enclosing class, for the parameter +types of the functional interface, as for `new X(args)` in the same class and +as in Java, and it is called directly. + +**Who is affected.** Statically compiled constructor references whose result +depended on that run-time selection: + +- The overload is chosen by the static parameter types of the functional + interface. `Function f = X::new` calls `X(CharSequence)` even + when it is given a `String`. With `IL(Object)` and `IL(long)`, + `Function` calls `IL(Object)`, where it called `IL(long)`; with + `IL2(int)` and `IL2(Integer)`, `IntFunction` calls `IL2(int)`, where it + called `IL2(Integer)`. +- An `Object[]` given to `Function` for `VO(Object...)` is passed as + one element, where its elements were passed. +- `null` given to `Function` for `PI(int)` throws + `NullPointerException`, where it threw `GroovyRuntimeException`. +- A constructor the enclosing class cannot call no longer wins over one it can. + With `private Foo(String)` and `public Foo(Object)`, `Function` + in another class calls `Foo(Object)`, as `new Foo(s)` does. +- A constructor added or overridden through the meta-class, for example with + `ExpandoMetaClass`, is not used, as for a statically compiled `new`. + +**What is unchanged.** The constructor is still selected at run time when no +single constructor matches the parameter types (GROOVY-10971), for an enum, for +a protected constructor of another package, and for a protected or +package-private constructor of a class that a `GroovyClassLoader` defined in +another compilation, such as a class of an earlier `GroovyShell` parse. Dynamic +and `@TypeChecked` code select the constructor at run time as before. + ## The binary-compatibility check The [`subprojects/binary-compatibility/`](subprojects/binary-compatibility) diff --git a/src/spec/doc/core-operators.adoc b/src/spec/doc/core-operators.adoc index 7f25e942916..58c48d16c61 100644 --- a/src/spec/doc/core-operators.adoc +++ b/src/spec/doc/core-operators.adoc @@ -548,6 +548,13 @@ include::../test/OperatorsTest.groovy[tags=constructor_refs,indent=0] <1> class constructor reference <2> array constructor reference +Under static compilation, a constructor reference calls the constructor that `new` would call with the parameter +types of the functional interface, selected at compile time among the constructors visible from the enclosing class. +It is selected at run time instead, through the meta-class, when no single constructor matches those types, for an +enum, for a protected constructor of another package, and for a protected or package-private constructor of a class +that a `GroovyClassLoader` defined in another compilation, such as a class of an earlier `GroovyShell` parse. In a +GraalVM native image, these run-time cases still need reflection metadata for the constructors. + == Regular expression operators === Pattern operator From 73b3cc88636d9398923f5303768b25d621a26df1 Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Thu, 29 Oct 2026 11:28:30 -0700 Subject: [PATCH 15/15] GROOVY-12488: say what the in-memory mark means for a GroovyClassLoader subclass GroovyClassLoader marks every parseClass compilation as defined in memory, whatever its class collector does. A subclass whose collector defines the classes with the GroovyClassLoader itself therefore takes the meta-class path for non-public members of classes of its other compilations, where a direct access would also work. The javadoc of the mark now says so. --- src/main/java/groovy/lang/GroovyClassLoader.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/groovy/lang/GroovyClassLoader.java b/src/main/java/groovy/lang/GroovyClassLoader.java index c44dd5f6734..5365f2ff9c7 100644 --- a/src/main/java/groovy/lang/GroovyClassLoader.java +++ b/src/main/java/groovy/lang/GroovyClassLoader.java @@ -454,6 +454,13 @@ private Class doParseClass(final GroovyCodeSource codeSource) { * Records that the classes of the compilation unit are defined by an * {@link InnerLoader} of their own, so that the compiler does not assume * that a class of another compilation unit is in the same run-time package. + *

+ * Every compilation of {@code parseClass} is marked, whatever its + * {@link ClassCollector} does with the classes. A subclass whose collector + * defines them with this class loader itself, rather than with an + * {@code InnerLoader}, therefore reaches the non-public members of classes + * of its other compilations through the meta-class where a direct access + * would also work; that is slower, but not wrong. */ private static void markDefinedInMemory(final CompilationUnit unit) { unit.getAST().putNodeMetaData(InnerLoader.class, Boolean.TRUE);