diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index e170acfc2ba..8098bdfda91 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -809,6 +809,41 @@ of `CompilationUnit` that adds it as a phase operation, or that calls `PlaceholderVisitor.isPlaceholder`. Drop the phase operation. Where code needs to know whether a variable is `_`, check its name. +### 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/main/java/groovy/lang/GroovyClassLoader.java b/src/main/java/groovy/lang/GroovyClassLoader.java index a382c783108..5365f2ff9c7 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,22 @@ 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. + *

+ * 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); + } + 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..51595a53815 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,41 @@ 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. + *

+ * {@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 + * @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 47a45f2f04f..2e23a092c6a 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 @@ -176,6 +176,29 @@ 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. + *

+ * 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; + 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 2afdfbf0731..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 @@ -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; @@ -69,6 +70,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,26 +462,63 @@ private MethodNode addSyntheticMethodForConstructorReference(final String method ArgumentListExpression ctorArgs = new ArgumentListExpression(parametersWithExactType); Expression returnValue; - if (returnType.isArray()) { + boolean staticCompile = false; + 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); + 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, 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 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); + // 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 + && StaticInvocationWriter.isDirectlyAccessible(ctor, controller.getClassNode())) { + return ctor; + } + return null; + } + 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 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, exceptions, 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 diff --git a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy index eca241769bb..13a8f64386a 100644 --- a/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy +++ b/src/test/groovy/groovy/transform/stc/MethodReferenceTest.groovy @@ -19,11 +19,17 @@ 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.util.regex.Pattern + import static groovy.test.GroovyAssert.assertScript import static groovy.test.GroovyAssert.shouldFail @@ -1146,6 +1152,501 @@ final class MethodReferenceTest { assert err.message =~ /Cannot call java.lang.Integer\[\]#\(int\) with arguments / } + // GROOVY-12488 + @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() + ''' + } + + // GROOVY-12488 + @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() + ''' + } + + // GROOVY-12488 + @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() + ''' + } + + // GROOVY-12488 + @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' + ''' + } + + // GROOVY-12488 + @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() + ''' + } + + // GROOVY-12488 + @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, GROOVY-12488 + @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() + ''' + } + + // GROOVY-12488 + @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' + ''' + } + + // GROOVY-12488 + @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() + ''' + } + + // 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() + ''' + } + + // 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' + ''' + } + + // 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 // 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 // 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() { + 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 } + 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' + + // 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) } + ''' + } + @Test // arrayClass::new void testIntFunctionCN() { assertScript shell, ''' @@ -1160,6 +1661,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, ''' @@ -2698,15 +3241,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 +3261,465 @@ final class MethodReferenceTest { ) } + // GROOVY-12488 + @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) + } + + // GROOVY-12488 + @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) + } + + // GROOVY-12488 + @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) + } + + // GROOVY-12488 + @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) + } + + // GROOVY-12488 + @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-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 = compileAgainstLibrary('''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, GROOVY-12488: 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;']) + } + + // GROOVY-12488 + @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;']) + } + + // 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 = compileAgainstLibrary(library, 'p.C', 'ctorRef$prot$0', script) + assert prot.hasSequence(['INVOKEDYNAMIC init(']) + assert !prot.hasSequence(['INVOKESPECIAL p/A.']) + + def pkg = compileAgainstLibrary(library, 'p.C', 'ctorRef$pkg$1', script) + assert pkg.hasSequence(['INVOKEDYNAMIC init(']) + 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 = extractMethodSequence(bytes, 'ctorRef$prot$0') + assert prot.hasSequence(['NEW p/A', 'DUP', 'ALOAD 0', 'INVOKESPECIAL p/A. (Ljava/lang/String;)V']) + 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 + 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() + } + } + + // 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 = 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 = compileAgainstLibrary(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 = compileAgainstLibrary('''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 = ''' @@ -2744,6 +3747,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 = ''' @@ -2803,10 +3855,30 @@ 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, final String className = 'C') { + selectMethod(method, compileStaticBytecode(classNamePattern: Pattern.quote(className), method: method, script)) + } + + /** + * 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 final String COMMON_IMPORTS = '''\ import groovy.transform.CompileStatic import java.io.Serializable diff --git a/src/test/groovy/org/codehaus/groovy/classgen/asm/AbstractBytecodeTestCase.groovy b/src/test/groovy/org/codehaus/groovy/classgen/asm/AbstractBytecodeTestCase.groovy index 3741c63144b..bca45b6b940 100644 --- a/src/test/groovy/org/codehaus/groovy/classgen/asm/AbstractBytecodeTestCase.groovy +++ b/src/test/groovy/org/codehaus/groovy/classgen/asm/AbstractBytecodeTestCase.groovy @@ -194,6 +194,61 @@ abstract class AbstractBytecodeTestCase { new InstructionSequence(instructions: out.toString().split('\n')*.trim()) } + /** + * Extracts the instructions of one method from raw class bytes, without the + * selection markers that {@link #extractSequence} puts around them. + * + * @param bytes the raw class bytecode to analyze + * @param method the name of the method + * @return an InstructionSequence containing only the instructions of the method + */ + protected InstructionSequence extractMethodSequence(final byte[] bytes, final String method) { + selectMethod(method, extractSequence(bytes, [method: method])) + } + + /** + * Selects the instructions between the selection markers of a sequence that + * {@link #extractSequence} extracted for one method. + * + * @param method the name of the method, for the failure message + * @param sequence the extracted sequence + * @return an InstructionSequence containing only the instructions of the method + */ + protected 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)..