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

sunlan pushed a commit to branch GROOVY-7785
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 96822f3ad3d0ee86fc290c477521cc04e5602d33
Author: Daniel Sun <[email protected]>
AuthorDate: Tue Feb 3 01:22:39 2026 +0900

    GROOVY-7785: StackoverflowException when using too many chained method calls
---
 .../groovy/classgen/asm/InvocationWriter.java      |  82 ++++-
 .../classgen/asm/indy/InvokeDynamicWriter.java     |  93 ++++-
 .../groovy/control/StaticImportVisitor.java        |  21 +-
 src/test/groovy/bugs/Groovy7785.groovy             | 384 ++++++++++++++++++++-
 4 files changed, 567 insertions(+), 13 deletions(-)

diff --git 
a/src/main/java/org/codehaus/groovy/classgen/asm/InvocationWriter.java 
b/src/main/java/org/codehaus/groovy/classgen/asm/InvocationWriter.java
index f928abbc49..62b1bee97a 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/InvocationWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/InvocationWriter.java
@@ -384,9 +384,9 @@ public class InvocationWriter {
             controller.getSuperMethodNames().add(methodName); // for MOP method
         }
 
-        // receiver
+        // receiver - GROOVY-7785: use iterative approach for chained 
spread-safe calls
         compileStack.pushImplicitThis(implicitThis);
-        receiver.visit(acg);
+        visitReceiverOfMethodCall(receiver, spreadSafe);
         operandStack.box();
         compileStack.popImplicitThis();
 
@@ -425,6 +425,84 @@ public class InvocationWriter {
         operandStack.replace(ClassHelper.OBJECT_TYPE, operandsToRemove);
     }
 
+    /**
+     * Visits receiver expression, using iterative approach for spread-safe 
method call chains.
+     * GROOVY-7785: Flattens deep recursive AST structures to avoid stack 
overflow.
+     */
+    private void visitReceiverOfMethodCall(final Expression receiver, final 
boolean spreadSafe) {
+        if (!spreadSafe || !(receiver instanceof MethodCallExpression mce) || 
!mce.isSpreadSafe()) {
+            receiver.visit(controller.getAcg());
+            return;
+        }
+
+        List<MethodCallExpression> chain = new ArrayList<>();
+        Expression current = receiver;
+        while (current instanceof MethodCallExpression cmce && 
cmce.isSpreadSafe()) {
+            chain.add(cmce);
+            current = cmce.getObjectExpression();
+        }
+        Expression innermost = current;
+
+        innermost.visit(controller.getAcg());
+
+        AsmClassGenerator acg = controller.getAcg();
+        for (int i = chain.size() - 1; i >= 0; i--) {
+            MethodCallExpression call = chain.get(i);
+            acg.onLineNumber(call, "visitMethodCallExpression: \"" + 
call.getMethod() + "\":");
+            processChainedSpreadSafeCall(call);
+            controller.getAssertionWriter().record(call.getMethod());
+        }
+    }
+
+    /**
+     * Processes a spread-safe method call with receiver already on stack.
+     */
+    private void processChainedSpreadSafeCall(final MethodCallExpression call) 
{
+        OperandStack operandStack = controller.getOperandStack();
+        AsmClassGenerator acg = controller.getAcg();
+        CompileStack compileStack = controller.getCompileStack();
+
+        operandStack.box();
+        compileStack.pushLHS(false);
+
+        // Push sender and swap with receiver: [receiver] -> [sender, receiver]
+        new ClassExpression(controller.getClassNode()).visit(acg);
+        controller.getMethodVisitor().visitInsn(SWAP);
+
+        // message
+        Expression message = new CastExpression(ClassHelper.STRING_TYPE, 
call.getMethod());
+        message.visit(acg);
+        operandStack.box();
+
+        // arguments
+        Expression arguments = call.getArguments();
+        boolean containsSpread = 
AsmClassGenerator.containsSpreadExpression(arguments);
+        int numberOfArguments = containsSpread ? -1 : 
AsmClassGenerator.argumentSize(arguments);
+        int operandsToRemove = 3; // sender + receiver + message
+
+        if (numberOfArguments > MethodCallerMultiAdapter.MAX_ARGS || 
containsSpread) {
+            ArgumentListExpression ae = makeArgumentList(arguments);
+            if (containsSpread) {
+                acg.despreadList(ae.getExpressions(), true);
+            } else {
+                ae.visit(acg);
+            }
+        } else if (numberOfArguments > 0) {
+            operandsToRemove += numberOfArguments;
+            TupleExpression te = (TupleExpression) arguments;
+            for (int i = 0; i < numberOfArguments; i++) {
+                Expression arg = te.getExpression(i);
+                arg.visit(acg);
+                operandStack.box();
+                if (arg instanceof CastExpression) acg.loadWrapper(arg);
+            }
+        }
+
+        invokeMethod.call(controller.getMethodVisitor(), numberOfArguments, 
call.isSafe(), call.isSpreadSafe());
+        compileStack.popLHS();
+        operandStack.replace(ClassHelper.OBJECT_TYPE, operandsToRemove);
+    }
+
     /**
      * if Class.forName(x) is recognized, make a direct method call
      */
diff --git 
a/src/main/java/org/codehaus/groovy/classgen/asm/indy/InvokeDynamicWriter.java 
b/src/main/java/org/codehaus/groovy/classgen/asm/indy/InvokeDynamicWriter.java
index 059f20bfc5..e5833f9797 100644
--- 
a/src/main/java/org/codehaus/groovy/classgen/asm/indy/InvokeDynamicWriter.java
+++ 
b/src/main/java/org/codehaus/groovy/classgen/asm/indy/InvokeDynamicWriter.java
@@ -25,6 +25,7 @@ import org.codehaus.groovy.ast.expr.ConstantExpression;
 import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
 import org.codehaus.groovy.ast.expr.EmptyExpression;
 import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
 import org.codehaus.groovy.ast.expr.PropertyExpression;
 import org.codehaus.groovy.ast.tools.WideningCategories;
 import org.codehaus.groovy.classgen.AsmClassGenerator;
@@ -42,8 +43,10 @@ import org.objectweb.asm.Opcodes;
 import java.lang.invoke.CallSite;
 import java.lang.invoke.MethodHandles.Lookup;
 import java.lang.invoke.MethodType;
+import java.util.ArrayList;
 import java.util.List;
 
+import static org.apache.groovy.ast.tools.ExpressionUtils.isSuperExpression;
 import static org.apache.groovy.ast.tools.ExpressionUtils.isThisExpression;
 import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE;
 import static org.codehaus.groovy.ast.ClassHelper.boolean_TYPE;
@@ -54,16 +57,16 @@ import static 
org.codehaus.groovy.ast.ClassHelper.isWrapperBoolean;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.bytecodeX;
 import static org.codehaus.groovy.classgen.asm.BytecodeHelper.doCast;
 import static 
org.codehaus.groovy.classgen.asm.BytecodeHelper.getTypeDescription;
-import static org.codehaus.groovy.vmplugin.v8.IndyInterface.GROOVY_OBJECT;
-import static org.codehaus.groovy.vmplugin.v8.IndyInterface.IMPLICIT_THIS;
-import static org.codehaus.groovy.vmplugin.v8.IndyInterface.SAFE_NAVIGATION;
-import static org.codehaus.groovy.vmplugin.v8.IndyInterface.SPREAD_CALL;
-import static org.codehaus.groovy.vmplugin.v8.IndyInterface.THIS_CALL;
 import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.CAST;
 import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.GET;
 import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.INIT;
 import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.INTERFACE;
 import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.METHOD;
+import static org.codehaus.groovy.vmplugin.v8.IndyInterface.GROOVY_OBJECT;
+import static org.codehaus.groovy.vmplugin.v8.IndyInterface.IMPLICIT_THIS;
+import static org.codehaus.groovy.vmplugin.v8.IndyInterface.SAFE_NAVIGATION;
+import static org.codehaus.groovy.vmplugin.v8.IndyInterface.SPREAD_CALL;
+import static org.codehaus.groovy.vmplugin.v8.IndyInterface.THIS_CALL;
 import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
 import static org.objectweb.asm.Opcodes.IFNULL;
 
@@ -113,12 +116,90 @@ public class InvokeDynamicWriter extends InvocationWriter 
{
 
         // load normal receiver as first argument
         compileStack.pushImplicitThis(implicitThis);
-        receiver.visit(controller.getAcg());
+        // GROOVY-7785: use iterative approach to avoid stack overflow for 
chained method calls
+        visitReceiverOfMethodCall(receiver);
         compileStack.popImplicitThis();
 
         return "(" + getTypeDescription(operandStack.getTopOperand());
     }
 
+    /**
+     * Visits receiver expression, using iterative approach for method call 
chains.
+     * GROOVY-7785: Flattens deep recursive AST structures to avoid stack 
overflow.
+     */
+    private void visitReceiverOfMethodCall(final Expression receiver) {
+        // Collect chain of simple method calls that can use indy optimization
+        List<MethodCallExpression> chain = new ArrayList<>();
+        Expression current = receiver;
+        while (current instanceof MethodCallExpression mce
+                && !mce.isSpreadSafe() && !mce.isImplicitThis()
+                && !isSuperExpression(mce.getObjectExpression())
+                && !isThisExpression(mce.getObjectExpression())) {
+            String name = getMethodName(mce.getMethod());
+            if (name == null || "call".equals(name)) break; // dynamic name or 
functional interface call
+            chain.add(mce);
+            current = mce.getObjectExpression();
+        }
+
+        if (chain.isEmpty()) {
+            receiver.visit(controller.getAcg());
+            return;
+        }
+
+        current.visit(controller.getAcg());
+        AsmClassGenerator acg = controller.getAcg();
+        for (int i = chain.size() - 1; i >= 0; i--) {
+            MethodCallExpression call = chain.get(i);
+            acg.onLineNumber(call, "visitMethodCallExpression: \"" + 
call.getMethod() + "\":");
+            finishIndyCallForChain(call);
+            controller.getAssertionWriter().record(call.getMethod());
+        }
+    }
+
+    /**
+     * Completes an indy call for a chained method with receiver already on 
stack.
+     */
+    private void finishIndyCallForChain(final MethodCallExpression call) {
+        OperandStack operandStack = controller.getOperandStack();
+        AsmClassGenerator acg = controller.getAcg();
+        Expression arguments = call.getArguments();
+        boolean safe = call.isSafe();
+
+        StringBuilder sig = new 
StringBuilder("(").append(getTypeDescription(operandStack.getTopOperand()));
+        Label end = null;
+        if (safe && !isPrimitiveType(operandStack.getTopOperand())) {
+            operandStack.dup();
+            end = operandStack.jump(IFNULL);
+        }
+
+        int nArgs = 1;
+        List<Expression> args = makeArgumentList(arguments).getExpressions();
+        boolean spread = AsmClassGenerator.containsSpreadExpression(arguments);
+        if (spread) {
+            acg.despreadList(args, true);
+            sig.append(getTypeDescription(Object[].class));
+        } else {
+            for (Expression arg : args) {
+                arg.visit(acg);
+                if (arg instanceof CastExpression) {
+                    operandStack.box();
+                    acg.loadWrapper(arg);
+                    sig.append(getTypeDescription(Wrapper.class));
+                } else {
+                    
sig.append(getTypeDescription(operandStack.getTopOperand()));
+                }
+                nArgs++;
+            }
+        }
+        sig.append(")Ljava/lang/Object;");
+
+        int flags = safe ? SAFE_NAVIGATION : 0;
+        if (spread) flags |= SPREAD_CALL;
+        
controller.getMethodVisitor().visitInvokeDynamicInsn(METHOD.getCallSiteName(), 
sig.toString(), BSM, getMethodName(call.getMethod()), flags);
+        operandStack.replace(OBJECT_TYPE, nArgs);
+        if (end != null) controller.getMethodVisitor().visitLabel(end);
+    }
+
     private void finishIndyCall(final Handle bsmHandle, final String 
methodName, final String sig, final int numberOfArguments, final Object... 
bsmArgs) {
         CompileStack compileStack = controller.getCompileStack();
         OperandStack operandStack = controller.getOperandStack();
diff --git a/src/main/java/org/codehaus/groovy/control/StaticImportVisitor.java 
b/src/main/java/org/codehaus/groovy/control/StaticImportVisitor.java
index f167f95690..147ce3733b 100644
--- a/src/main/java/org/codehaus/groovy/control/StaticImportVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/StaticImportVisitor.java
@@ -49,6 +49,8 @@ import org.codehaus.groovy.ast.expr.VariableExpression;
 import org.codehaus.groovy.ast.stmt.Statement;
 import org.codehaus.groovy.syntax.Types;
 
+import java.util.ArrayDeque;
+import java.util.Deque;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Predicate;
@@ -245,7 +247,24 @@ public class StaticImportVisitor extends 
ClassCodeExpressionTransformer {
     }
 
     protected Expression transformMethodCallExpression(MethodCallExpression 
mce) {
-        Expression object = transform(mce.getObjectExpression());
+        // GROOVY-7785: use iteration to handle chained method calls to avoid 
StackOverflowError
+        Deque<MethodCallExpression> chain = new ArrayDeque<>();
+        Expression current = mce;
+        while (current instanceof MethodCallExpression currentMce) {
+            chain.push(currentMce);
+            current = currentMce.getObjectExpression();
+        }
+        // transform the innermost non-method-call object expression
+        Expression object = transform(current);
+        // process the chain from innermost to outermost
+        while (!chain.isEmpty()) {
+            MethodCallExpression currentMce = chain.pop();
+            object = transformSingleMethodCallExpression(currentMce, object);
+        }
+        return object;
+    }
+
+    private Expression 
transformSingleMethodCallExpression(MethodCallExpression mce, Expression 
object) {
         Expression method = transform(mce.getMethod());
         Expression args = transform(mce.getArguments());
 
diff --git a/src/test/groovy/bugs/Groovy7785.groovy 
b/src/test/groovy/bugs/Groovy7785.groovy
index ea9d60d59c..ce24240552 100644
--- a/src/test/groovy/bugs/Groovy7785.groovy
+++ b/src/test/groovy/bugs/Groovy7785.groovy
@@ -18,18 +18,394 @@
  */
 package bugs
 
+import org.junit.jupiter.api.Disabled
 import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable
 
 import static groovy.test.GroovyAssert.assertScript
 
-@DisabledIfEnvironmentVariable(named = "CI", matches = ".*") // runs locally 
but fails in CI, more investigation needed
+/**
+ * Tests for GROOVY-7785: StackOverflowError with deeply nested chained method 
calls.
+ * Each test uses chained method calls to verify the fix handles deep chains.
+ */
 final class Groovy7785 {
+
     @Test
     void testManyChainedMethodCalls() {
+        // 1000 chained append calls on StringBuilder
+        assertScript '''
+            def r = new 
StringBuilder().append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("
 [...]
+            assert r.toString() == 'a' * 1000
+        '''
+    }
+
+    @Test
+    void testChainedCaseConversions() {
+        // 1000 chained toUpperCase/toLowerCase calls
+        assertScript '''
+            def result = 
"hello".toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase().toLowerCase().toUpperCase
 [...]
+            assert result == "hello"  // 1000 is even, ends with toLowerCase
+        '''
+    }
+
+    @Test
+    void testChainedReplaceOperations() {
+        // 1000 chained replace calls (none match, string unchanged)
+        assertScript '''
+            def result = "test string".replace("NOTFOUND0", 
"X").replace("NOTFOUND1", "X").replace("NOTFOUND2", "X").replace("NOTFOUND3", 
"X").replace("NOTFOUND4", "X").replace("NOTFOUND5", "X").replace("NOTFOUND6", 
"X").replace("NOTFOUND7", "X").replace("NOTFOUND8", "X").replace("NOTFOUND9", 
"X").replace("NOTFOUND10", "X").replace("NOTFOUND11", 
"X").replace("NOTFOUND12", "X").replace("NOTFOUND13", 
"X").replace("NOTFOUND14", "X").replace("NOTFOUND15", 
"X").replace("NOTFOUND16", "X").repl [...]
+            assert result == "test string"
+        '''
+    }
+
+    @Test
+    void testChainedTrimOperations() {
+        // 1000 chained trim calls (idempotent operation)
+        assertScript '''
+            def result = "  hello world  
".trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().t
 [...]
+            assert result == "hello world"
+        '''
+    }
+
+    @Test
+    void testChainedCollectionOperations() {
+        // 1000 chained collect calls on a list
+        assertScript '''
+            def result = [1, 2, 3].collect { it }.collect { it }.collect { it 
}.collect { it }.collect { it }.collect { it }.collect { it }.collect { it 
}.collect { it }.collect { it }.collect { it }.collect { it }.collect { it 
}.collect { it }.collect { it }.collect { it }.collect { it }.collect { it 
}.collect { it }.collect { it }.collect { it }.collect { it }.collect { it 
}.collect { it }.collect { it }.collect { it }.collect { it }.collect { it 
}.collect { it }.collect { it }.collect [...]
+            assert result == [1, 2, 3]
+        '''
+    }
+
+    @Test
+    void testChainedBuilderPattern() {
+        // 1000 chained builder set calls
+        assertScript '''
+            class Builder {
+                private map = [:]
+                Builder set(String key, value) {
+                    map[key] = value
+                    return this
+                }
+                Map build() { map }
+            }
+
+            def result = new Builder().set("k0", 0).set("k1", 1).set("k2", 
2).set("k3", 3).set("k4", 4).set("k5", 5).set("k6", 6).set("k7", 7).set("k8", 
8).set("k9", 9).set("k10", 10).set("k11", 11).set("k12", 12).set("k13", 
13).set("k14", 14).set("k15", 15).set("k16", 16).set("k17", 17).set("k18", 
18).set("k19", 19).set("k20", 20).set("k21", 21).set("k22", 22).set("k23", 
23).set("k24", 24).set("k25", 25).set("k26", 26).set("k27", 27).set("k28", 
28).set("k29", 29).set("k30", 30).set("k31 [...]
+            assert result.size() == 1000
+            assert result.k0 == 0
+            assert result.k999 == 999
+        '''
+    }
+
+    @Test
+    void testChainedAppendWithDifferentTypes() {
+        // 1000 chained appends with mixed content (400x + 400y + 200z)
+        assertScript '''
+            def sb = new 
StringBuilder().append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append("x").append(
 [...]
+            assert sb.length() == 1000
+            assert sb.toString().startsWith("x" * 400)
+            assert sb.toString().endsWith("z" * 200)
+        '''
+    }
+
+    @Test
+    void testChainedNavigationDeep() {
+        // 1000 chained getNext() calls
+        assertScript '''
+            class Node {
+                Node next
+                String value = "v"
+                Node getNext() { next }
+            }
+
+            // Build a chain of 1001 nodes
+            def root = new Node()
+            def current = root
+            1000.times {
+                current.next = new Node()
+                current = current.next
+            }
+
+            // Navigate through 1000 nodes
+            def nav = 
root.getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNext().getNe
 [...]
+            assert nav.value == "v"
+            assert nav.next == null
+        '''
+    }
+
+    @Test
+    void testChainedStreamOperations() {
+        // 1000 chained stream map operations
+        assertScript '''
+            import java.util.stream.Collectors
+
+            def result = [1, 2, 3].stream().map { it }.map { it }.map { it 
}.map { it }.map { it }.map { it }.map { it }.map { it }.map { it }.map { it 
}.map { it }.map { it }.map { it }.map { it }.map { it }.map { it }.map { it 
}.map { it }.map { it }.map { it }.map { it }.map { it }.map { it }.map { it 
}.map { it }.map { it }.map { it }.map { it }.map { it }.map { it }.map { it 
}.map { it }.map { it }.map { it }.map { it }.map { it }.map { it }.map { it 
}.map { it }.map { it }.map { it [...]
+            assert result == [1, 2, 3]
+        '''
+    }
+
+    @Test
+    void testChainedOptionalOperations() {
+        // 1000 chained Optional map operations
+        assertScript '''
+            def result = Optional.of(42).map { it }.map { it }.map { it }.map 
{ it }.map { it }.map { it }.map { it }.map { it }.map { it }.map { it }.map { 
it }.map { it }.map { it }.map { it }.map { it }.map { it }.map { it }.map { it 
}.map { it }.map { it }.map { it }.map { it }.map { it }.map { it }.map { it 
}.map { it }.map { it }.map { it }.map { it }.map { it }.map { it }.map { it 
}.map { it }.map { it }.map { it }.map { it }.map { it }.map { it }.map { it 
}.map { it }.map { it }. [...]
+            assert result == 42
+        '''
+    }
+
+    @Test
+    void testChainedFindAllOperations() {
+        // 1000 chained findAll with always-true predicate
+        assertScript '''
+            def list = [1, 2, 3, 4, 5].findAll { true }.findAll { true 
}.findAll { true }.findAll { true }.findAll { true }.findAll { true }.findAll { 
true }.findAll { true }.findAll { true }.findAll { true }.findAll { true 
}.findAll { true }.findAll { true }.findAll { true }.findAll { true }.findAll { 
true }.findAll { true }.findAll { true }.findAll { true }.findAll { true 
}.findAll { true }.findAll { true }.findAll { true }.findAll { true }.findAll { 
true }.findAll { true }.findAll { t [...]
+            assert list == [1, 2, 3, 4, 5]
+        '''
+    }
+
+    @Test
+    void testMixedChainedOperations() {
+        // 1000 mixed string operations (trim/toString/replace/intern)
+        assertScript '''
+            def result = "HELLO".trim().toString().replace("NOTFOUND2", 
"X").intern().trim().toString().replace("NOTFOUND6", 
"X").intern().trim().toString().replace("NOTFOUND10", 
"X").intern().trim().toString().replace("NOTFOUND14", 
"X").intern().trim().toString().replace("NOTFOUND18", 
"X").intern().trim().toString().replace("NOTFOUND22", 
"X").intern().trim().toString().replace("NOTFOUND26", 
"X").intern().trim().toString().replace("NOTFOUND30", 
"X").intern().trim().toString().replace("NO [...]
+            assert result.toLowerCase() == "hello"
+        '''
+    }
+
+    @Test
+    void testChainedWithClosureReturningThis() {
+        // 1000 chained increment operations
+        assertScript '''
+            class Counter {
+                int count = 0
+                Counter increment() { count++; this }
+            }
+
+            def c = new 
Counter().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().increment().in
 [...]
+            assert c.count == 1000
+        '''
+    }
+
+
+    @Test
+    void testChainedCallOperations() {
+        assertScript '''
+            def str = "  hello world  "
+            def result = 
str.trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().t
 [...]
+            assert result == "hello world"
+        '''
+    }
+
+    @Test
+    void testChainedCallOperationsInClass() {
+        assertScript '''
+            class X {
+                static void test() {
+                    def str = "  hello world  "
+                    def result = 
str.trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().
 [...]
+                    assert result == "hello world"
+                }
+            }
+            X.test()
+        '''
+    }
+
+    @Test
+    void testChainedCallOperationsInClass_2() {
+        assertScript '''
+            class X {
+                static void test() {
+                    X x = new X()
+                    x.doProcess()
+                }
+                void doProcess() {
+                    def str = "  hello world  "
+                    def result = process(str)
+                    assert result == "hello world"
+                }
+                def process(str) {
+                    def result = 
str.trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().
 [...]
+                    return result
+                }
+            }
+            X.test()
+        '''
+    }
+
+    /**
+     * It takes too long to compile, disable it for now.
+     */
+    @Disabled
+    @Test
+    void testChainedCallOperationsInClass_2_CS() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            class X {
+                static void test() {
+                    X x = new X()
+                    x.doProcess()
+                }
+                void doProcess() {
+                    def str = "  hello world  "
+                    def result = process(str)
+                    assert result == "hello world"
+                }
+                def process(str) {
+                    def result = 
str.trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().trim().
 [...]
+                    return result
+                }
+            }
+            X.test()
+        '''
+    }
+
+
+    @Test
+    void testChainedSafeCallOperations() {
+        // 1000 chained safe navigation calls (?.)
+        assertScript '''
+            def str = "  hello world  "
+            def result = 
str?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()
 [...]
+            assert result == "hello world"
+        '''
+    }
+
+    @Test
+    void testChainedSafeCallOperationsInClass() {
+        // 1000 chained safe navigation calls (?.)
+        assertScript '''
+            class X {
+                static void test() {
+                    def str = "  hello world  "
+                    def result = 
str?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()
 [...]
+                    assert result == "hello world"
+                }
+            }
+            X.test()
+        '''
+    }
+
+    @Test
+    void testChainedSafeCallOperationsInClass_2() {
+        // 1000 chained safe navigation calls (?.)
+        assertScript '''
+            class X {
+                static void test() {
+                    X x = new X()
+                    x.doProcess()
+                }
+                void doProcess() {
+                    def str = "  hello world  "
+                    def result = process(str)
+                    assert result == "hello world"
+                }
+                def process(str) {
+                    def result = 
str?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()
 [...]
+                    return result
+                }
+            }
+            X.test()
+        '''
+    }
+
+    /**
+     * It takes too long to compile, disable it for now.
+     */
+    @Disabled
+    @Test
+    void testChainedSafeCallOperationsInClass_2_CS() {
+        // 1000 chained safe navigation calls (?.)
+        assertScript '''
+            @groovy.transform.CompileStatic
+            class X {
+                static void test() {
+                    X x = new X()
+                    x.doProcess()
+                }
+                void doProcess() {
+                    def str = "  hello world  "
+                    def result = process(str)
+                    assert result == "hello world"
+                }
+                def process(str) {
+                    def result = 
str?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()?.trim()
 [...]
+                    return result
+                }
+            }
+            X.test()
+        '''
+    }
+
+    @Test
+    void testChainedSpreadCallOperations() {
+        assertScript '''
+            def list = ["hello", "world", "groovy"]
+            def result = list*.toUpperCase()
+                            
*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()
 [...]
+                            
*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()
 [...]
+            assert result == ["HELLO", "WORLD", "GROOVY"]
+        '''
+    }
+
+    @Test
+    void testChainedSpreadCallOperationsInClass() {
+        assertScript '''
+            class X {
+                static void test() {
+                    def list = ["hello", "world", "groovy"]
+                    def result = list*.toUpperCase()
+                                    
*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()
 [...]
+                                    
*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()
 [...]
+                    assert result == ["HELLO", "WORLD", "GROOVY"]
+                }
+            }
+            X.test()
+        '''
+    }
+
+    @Test
+    void testChainedSpreadCallOperationsInClass_2() {
+        assertScript '''
+            class X {
+                static void test() {
+                    def x = new X()
+                    x.doProcess()
+                }
+                String doProcess() {
+                    def list = ["hello", "world", "groovy"]
+                    def result = process(list)
+                    assert result == ["HELLO", "WORLD", "GROOVY"]
+                }
+                def process(list) {
+                    def result = list*.toUpperCase()
+                                    
*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()
 [...]
+                                    
*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()
 [...]
+                    return result
+                }
+            }
+            X.test()
+        '''
+    }
+
+    @Disabled
+    @Test
+    void testChainedSpreadCallOperationsInClass_2_CS() {
         assertScript '''
-            def r = new 
StringBuilder().append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("a").append("
 [...]
-            assert r.toString() == 'a' * 771
+            @groovy.transform.CompileStatic
+            class X {
+                static void test() {
+                    def x = new X()
+                    x.doProcess()
+                }
+                String doProcess() {
+                    def list = ["hello", "world", "groovy"]
+                    def result = process(list)
+                    assert result == ["HELLO", "WORLD", "GROOVY"]
+                }
+                def process(list) {
+                    def result = list*.toUpperCase()
+                                    
*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()
 [...]
+                                    
*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()*.trim()
 [...]
+                    return result
+                }
+            }
+            X.test()
         '''
     }
 }


Reply via email to