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

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

commit b83cab100ed796d4488ab278311c7e640219554f
Author: Eric Milles <[email protected]>
AuthorDate: Mon Mar 31 19:46:39 2025 -0500

    GROOVY-10683: support `for (index, item in list)` looping
---
 src/antlr/GroovyParser.g4                          |  6 +++-
 .../apache/groovy/parser/antlr4/AstBuilder.java    | 36 +++++++++++++------
 .../org/codehaus/groovy/ast/stmt/ForStatement.java | 42 +++++++++++++++++-----
 .../groovy/classgen/VariableScopeVisitor.java      | 11 ++++--
 .../groovy/classgen/VerifierCodeVisitor.java       | 12 +++++--
 .../groovy/classgen/asm/StatementWriter.java       | 15 +++++---
 .../asm/sc/StaticTypesStatementWriter.java         | 29 ++++++++++-----
 .../transform/CategoryASTTransformation.java       |  6 +++-
 .../transform/sc/StaticCompilationVisitor.java     |  7 ++--
 src/spec/test/SemanticsTest.groovy                 |  5 +++
 src/test/groovy/ForLoopTest.groovy                 | 22 ++++++++++++
 .../groovy/parser/antlr4/util/AstDumper.groovy     |  8 +++--
 .../console/ui/AstNodeToScriptAdapter.groovy       | 17 +++++----
 13 files changed, 164 insertions(+), 52 deletions(-)

diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index edf75c4cde..b41faad333 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -691,7 +691,11 @@ forControl
     ;
 
 enhancedForControl
-    :   variableModifiersOpt type? identifier (COLON | IN) expression
+    :   (indexVariable COMMA)? variableModifiersOpt type? identifier (COLON | 
IN) expression
+    ;
+
+indexVariable
+    :   (BuiltInPrimitiveType | DEF | VAR)? identifier
     ;
 
 originalForControl
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java 
b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index e6903c80d4..2bef2bb19e 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -147,6 +147,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import static groovy.lang.Tuple.tuple;
@@ -469,15 +470,15 @@ public class AstBuilder extends 
GroovyParserBaseVisitor<Object> {
 
     @Override
     public ForStatement visitForStmtAlt(final ForStmtAltContext ctx) {
-        var controlElements = this.visitForControl(ctx.forControl());
+        Function<Statement, ForStatement> maker = 
this.visitForControl(ctx.forControl());
 
-        Statement loopBlock = this.unpackStatement((Statement) 
this.visit(ctx.statement()));
+        Statement loopBody = this.unpackStatement((Statement) 
this.visit(ctx.statement()));
 
-        return configureAST(new ForStatement(controlElements.getV1(), 
controlElements.getV2(), loopBlock), ctx);
+        return configureAST(maker.apply(loopBody), ctx);
     }
 
     @Override
-    public Tuple2<Parameter, Expression> visitForControl(final 
ForControlContext ctx) {
+    public Function<Statement, ForStatement> visitForControl(final 
ForControlContext ctx) {
         // e.g. `for (var e : x) { }` and `for (e in x) { }`
         EnhancedForControlContext enhancedCtx = ctx.enhancedForControl();
         if (asBoolean(enhancedCtx)) {
@@ -494,22 +495,37 @@ public class AstBuilder extends 
GroovyParserBaseVisitor<Object> {
     }
 
     @Override
-    public Tuple2<Parameter, Expression> visitEnhancedForControl(final 
EnhancedForControlContext ctx) {
-        var parameter = configureAST(new Parameter(this.visitType(ctx.type()), 
this.visitIdentifier(ctx.identifier())), ctx.identifier());
-        new ModifierManager(this, 
this.visitVariableModifiersOpt(ctx.variableModifiersOpt())).processParameter(parameter);
+    public Function<Statement, ForStatement> visitEnhancedForControl(final 
EnhancedForControlContext ctx) {
+        var indexParameter = asBoolean(ctx.indexVariable()) ? 
this.visitIndexVariable(ctx.indexVariable()) : null;
 
-        return tuple(parameter, (Expression) this.visit(ctx.expression()));
+        var valueParameter = configureAST(new 
Parameter(this.visitType(ctx.type()), this.visitIdentifier(ctx.identifier())), 
ctx.identifier());
+        ModifierManager modifierManager = new ModifierManager(this, 
this.visitVariableModifiersOpt(ctx.variableModifiersOpt()));
+        modifierManager.processParameter(valueParameter);
+
+        return (body) -> new ForStatement(indexParameter, valueParameter, 
(Expression) this.visit(ctx.expression()), body);
+    }
+
+    @Override
+    public Parameter visitIndexVariable(final IndexVariableContext ctx) {
+        var primitiveType = ctx.BuiltInPrimitiveType();
+        if (primitiveType != null && primitiveType.getText().length() != 3) {
+            throw this.createParsingFailedException(primitiveType.getText() + 
" is not allowed here", ctx);
+        }
+
+        var indexParameter = configureAST(new Parameter(ClassHelper.int_TYPE, 
this.visitIdentifier(ctx.identifier())), ctx.identifier());
+        indexParameter.setModifiers(Opcodes.ACC_FINAL);
+        return indexParameter;
     }
 
     @Override
-    public Tuple2<Parameter, Expression> visitOriginalForControl(final 
OriginalForControlContext ctx) {
+    public Function<Statement, ForStatement> visitOriginalForControl(final 
OriginalForControlContext ctx) {
         ClosureListExpression closureListExpression = new 
ClosureListExpression();
         closureListExpression.addExpression(this.visitForInit(ctx.forInit()));
         
closureListExpression.addExpression(Optional.ofNullable(ctx.expression())
           .map(e -> (Expression) 
this.visit(e)).orElse(EmptyExpression.INSTANCE));
         
closureListExpression.addExpression(this.visitForUpdate(ctx.forUpdate()));
 
-        return tuple(ForStatement.FOR_LOOP_DUMMY, closureListExpression);
+        return (body) -> new ForStatement(ForStatement.FOR_LOOP_DUMMY, 
closureListExpression, body);
     }
 
     @Override
diff --git a/src/main/java/org/codehaus/groovy/ast/stmt/ForStatement.java 
b/src/main/java/org/codehaus/groovy/ast/stmt/ForStatement.java
index 3cfdd9d455..40d3e5dd13 100644
--- a/src/main/java/org/codehaus/groovy/ast/stmt/ForStatement.java
+++ b/src/main/java/org/codehaus/groovy/ast/stmt/ForStatement.java
@@ -25,6 +25,8 @@ import org.codehaus.groovy.ast.Parameter;
 import org.codehaus.groovy.ast.VariableScope;
 import org.codehaus.groovy.ast.expr.Expression;
 
+import static java.util.Objects.requireNonNull;
+
 /**
  * Represents a for loop in Groovy.
  */
@@ -32,33 +34,57 @@ public class ForStatement extends Statement implements 
LoopingStatement {
 
     public static final Parameter FOR_LOOP_DUMMY = new 
Parameter(ClassHelper.OBJECT_TYPE, "forLoopDummyParameter");
 
-    private final Parameter variable;
+    private final Parameter indexVariable, valueVariable;
     private Expression collectionExpression;
     private Statement loopBlock;
 
-    public ForStatement(final Parameter variable, final Expression 
collectionExpression, final Statement loopBlock) {
-        this.variable = variable;
+    /**
+     * @since 5.0.0
+     */
+    public ForStatement(final Parameter indexVariable, final Parameter 
valueVariable, final Expression collectionExpression, final Statement 
loopBlock) {
+        this.indexVariable = indexVariable; // optional component
+        this.valueVariable = requireNonNull(valueVariable);
         setCollectionExpression(collectionExpression);
         setLoopBlock(loopBlock);
     }
 
+    public ForStatement(final Parameter variable, final Expression 
collectionExpression, final Statement loopBlock) {
+        this(null, variable, collectionExpression, loopBlock);
+    }
+
     public void setCollectionExpression(final Expression collectionExpression) 
{
-        this.collectionExpression = collectionExpression;
+        this.collectionExpression = requireNonNull(collectionExpression);
     }
 
     @Override
     public void setLoopBlock(final Statement loopBlock) {
-        this.loopBlock = loopBlock;
+        this.loopBlock = requireNonNull(loopBlock);
     }
 
     
//--------------------------------------------------------------------------
 
+    /**
+     * @since 5.0.0
+     */
+    public Parameter getIndexVariable() {
+        return indexVariable;
+    }
+
+    /**
+     * @since 5.0.0
+     */
+    public Parameter getValueVariable() {
+        return valueVariable != FOR_LOOP_DUMMY ? valueVariable : null;
+    }
+
+    @Deprecated(since = "5.0.0")
     public Parameter getVariable() {
-        return variable;
+        return valueVariable;
     }
 
+    @Deprecated(since = "5.0.0")
     public ClassNode getVariableType() {
-        return variable.getType();
+        return valueVariable.getType();
     }
 
     public Expression getCollectionExpression() {
@@ -79,7 +105,7 @@ public class ForStatement extends Statement implements 
LoopingStatement {
     }
 
     public void setVariableScope(final VariableScope scope) {
-       this.scope = scope;
+        this.scope = scope;
     }
 
     
//--------------------------------------------------------------------------
diff --git 
a/src/main/java/org/codehaus/groovy/classgen/VariableScopeVisitor.java 
b/src/main/java/org/codehaus/groovy/classgen/VariableScopeVisitor.java
index 2a610282fb..7aa4562e02 100644
--- a/src/main/java/org/codehaus/groovy/classgen/VariableScopeVisitor.java
+++ b/src/main/java/org/codehaus/groovy/classgen/VariableScopeVisitor.java
@@ -69,7 +69,9 @@ import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 import static java.lang.reflect.Modifier.isFinal;
@@ -546,9 +548,12 @@ public class VariableScopeVisitor extends 
ClassCodeVisitorSupport {
     public void visitForLoop(final ForStatement statement) {
         pushState();
         statement.setVariableScope(currentScope);
-        Parameter parameter = statement.getVariable();
-        parameter.setInStaticContext(currentScope.isInStaticContext());
-        if (parameter != ForStatement.FOR_LOOP_DUMMY) declare(parameter, 
statement);
+        Consumer<Parameter> define = (parameter) -> {
+            parameter.setInStaticContext(currentScope.isInStaticContext());
+            declare(parameter, statement);
+        };
+        Optional.ofNullable(statement.getIndexVariable()).ifPresent(define);
+        Optional.ofNullable(statement.getValueVariable()).ifPresent(define);
         super.visitForLoop(statement);
         popState();
     }
diff --git 
a/src/main/java/org/codehaus/groovy/classgen/VerifierCodeVisitor.java 
b/src/main/java/org/codehaus/groovy/classgen/VerifierCodeVisitor.java
index 4fa7dcc442..be87099c6a 100644
--- a/src/main/java/org/codehaus/groovy/classgen/VerifierCodeVisitor.java
+++ b/src/main/java/org/codehaus/groovy/classgen/VerifierCodeVisitor.java
@@ -21,6 +21,7 @@ package org.codehaus.groovy.classgen;
 import org.codehaus.groovy.ast.ASTNode;
 import org.codehaus.groovy.ast.ClassNode;
 import org.codehaus.groovy.ast.CodeVisitorSupport;
+import org.codehaus.groovy.ast.Variable;
 import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
 import org.codehaus.groovy.ast.expr.Expression;
 import org.codehaus.groovy.ast.expr.FieldExpression;
@@ -30,6 +31,8 @@ import org.codehaus.groovy.ast.expr.VariableExpression;
 import org.codehaus.groovy.ast.stmt.ForStatement;
 import org.codehaus.groovy.syntax.RuntimeParserException;
 
+import java.util.Optional;
+
 /**
  * Performs various checks on code inside methods and constructors
  * including checking for valid field, variables names etc. that
@@ -44,9 +47,12 @@ public class VerifierCodeVisitor extends CodeVisitorSupport {
     }
 
     @Override
-    public void visitForLoop(ForStatement expression) {
-        assertValidIdentifier(expression.getVariable().getName(), "for loop 
variable name", expression);
-        super.visitForLoop(expression);
+    public void visitForLoop(ForStatement statement) {
+        
Optional.ofNullable(statement.getIndexVariable()).map(Variable::getName)
+            .ifPresent(name -> assertValidIdentifier(name, "for loop index 
variable name", statement));
+        
Optional.ofNullable(statement.getValueVariable()).map(Variable::getName)
+            .ifPresent(name -> assertValidIdentifier(name, "for loop value 
variable name", statement));
+        super.visitForLoop(statement);
     }
 
     @Override
diff --git 
a/src/main/java/org/codehaus/groovy/classgen/asm/StatementWriter.java 
b/src/main/java/org/codehaus/groovy/classgen/asm/StatementWriter.java
index 710e8cf747..3a06162e0c 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/StatementWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/StatementWriter.java
@@ -93,7 +93,7 @@ public class StatementWriter {
     }
 
     public void writeForStatement(final ForStatement statement) {
-        if (statement.getVariable() == ForStatement.FOR_LOOP_DUMMY) {
+        if (statement.getCollectionExpression() instanceof 
ClosureListExpression) {
             writeForLoopWithClosureList(statement);
         } else {
             writeForInLoop(statement);
@@ -122,8 +122,12 @@ public class StatementWriter {
         MethodVisitor mv = controller.getMethodVisitor();
         OperandStack operandStack = controller.getOperandStack();
 
-        // declare the loop counter
-        BytecodeVariable variable = 
compileStack.defineVariable(statement.getVariable(), false);
+        // declare the loop index and value variables
+        BytecodeVariable indexVariable = 
Optional.ofNullable(statement.getIndexVariable()).map(iv -> {
+            mv.visitInsn(ICONST_M1); // initialize to -1 so increment can pair 
with next()
+            return compileStack.defineVariable(iv, true);
+        }).orElse(null);
+        BytecodeVariable valueVariable = 
compileStack.defineVariable(statement.getValueVariable(), false);
 
         // get the iterator and generate the loop control
         int iterator = compileStack.defineTemporaryVariable("iterator", 
ClassHelper.Iterator_TYPE, true);
@@ -141,7 +145,10 @@ public class StatementWriter {
         mv.visitVarInsn(ALOAD, iterator);
         writeIteratorNext(mv);
         operandStack.push(ClassHelper.OBJECT_TYPE);
-        operandStack.storeVar(variable);
+        operandStack.storeVar(valueVariable);
+        if (indexVariable != null) {
+            mv.visitIincInsn(indexVariable.getIndex(), 1);
+        }
 
         // generate the loop body
         statement.getLoopBlock().visit(controller.getAcg());
diff --git 
a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesStatementWriter.java
 
b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesStatementWriter.java
index b05c90dffc..d9fdd428ae 100644
--- 
a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesStatementWriter.java
+++ 
b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesStatementWriter.java
@@ -38,6 +38,7 @@ import org.objectweb.asm.MethodVisitor;
 
 import java.util.Enumeration;
 import java.util.Objects;
+import java.util.Optional;
 
 import static org.objectweb.asm.Opcodes.AALOAD;
 import static org.objectweb.asm.Opcodes.ALOAD;
@@ -50,6 +51,7 @@ import static org.objectweb.asm.Opcodes.FALOAD;
 import static org.objectweb.asm.Opcodes.GOTO;
 import static org.objectweb.asm.Opcodes.IALOAD;
 import static org.objectweb.asm.Opcodes.ICONST_0;
+import static org.objectweb.asm.Opcodes.ICONST_M1;
 import static org.objectweb.asm.Opcodes.IFEQ;
 import static org.objectweb.asm.Opcodes.IFNULL;
 import static org.objectweb.asm.Opcodes.IF_ICMPGE;
@@ -95,9 +97,8 @@ public class StaticTypesStatementWriter extends 
StatementWriter {
         ClassNode collectionType = 
controller.getTypeChooser().resolveType(collectionExpression, 
controller.getClassNode());
 
         int mark = operandStack.getStackLength();
-        Parameter loopVariable = loop.getVariable();
-        if (collectionType.isArray() && 
loopVariable.getType().equals(collectionType.getComponentType())) {
-            writeOptimizedForEachLoop(loop, loopVariable, 
collectionExpression, collectionType);
+        if (collectionType.isArray() && 
collectionType.getComponentType().equals(loop.getVariableType())) {
+            writeOptimizedForEachLoop(loop, collectionExpression, 
collectionType);
         } else if (GeneralUtils.isOrImplements(collectionType, 
ENUMERATION_CLASSNODE)) {
             writeEnumerationBasedForEachLoop(loop, collectionExpression, 
collectionType);
         } else {
@@ -107,13 +108,16 @@ public class StaticTypesStatementWriter extends 
StatementWriter {
         compileStack.pop();
     }
 
-    private void writeOptimizedForEachLoop(final ForStatement loop, final 
Parameter loopVariable, final Expression arrayExpression, final ClassNode 
arrayType) {
+    private void writeOptimizedForEachLoop(final ForStatement loop, final 
Expression arrayExpression, final ClassNode arrayType) {
         CompileStack compileStack = controller.getCompileStack();
         OperandStack operandStack = controller.getOperandStack();
         MethodVisitor mv = controller.getMethodVisitor();
         AsmClassGenerator acg = controller.getAcg();
 
-        BytecodeVariable variable = compileStack.defineVariable(loopVariable, 
arrayType.getComponentType(), false);
+        BytecodeVariable indexVariable = 
Optional.ofNullable(loop.getIndexVariable()).map(iv -> {
+            mv.visitInsn(ICONST_M1); return compileStack.defineVariable(iv, 
true);
+        }).orElse(null);
+        BytecodeVariable valueVariable = 
compileStack.defineVariable(loop.getValueVariable(), 
arrayType.getComponentType(), false);
         Label continueLabel = compileStack.getContinueLabel();
         Label breakLabel = compileStack.getBreakLabel();
 
@@ -141,10 +145,13 @@ public class StaticTypesStatementWriter extends 
StatementWriter {
         mv.visitJumpInsn(IF_ICMPGE, breakLabel);
 
         // get array element
-        loadFromArray(mv, operandStack, variable, array, loopIdx);
+        loadFromArray(mv, operandStack, valueVariable, array, loopIdx);
 
         // $idx += 1
         mv.visitIincInsn(loopIdx, 1);
+        if (indexVariable != null) {
+            mv.visitIincInsn(indexVariable.getIndex(), 1);
+        }
 
         // loop body
         loop.getLoopBlock().visit(acg);
@@ -190,7 +197,10 @@ public class StaticTypesStatementWriter extends 
StatementWriter {
         OperandStack operandStack = controller.getOperandStack();
         MethodVisitor mv = controller.getMethodVisitor();
 
-        BytecodeVariable variable = 
compileStack.defineVariable(loop.getVariable(), false);
+        BytecodeVariable indexVariable = 
Optional.ofNullable(loop.getIndexVariable()).map(iv -> {
+            mv.visitInsn(ICONST_M1); return compileStack.defineVariable(iv, 
true);
+        }).orElse(null);
+        BytecodeVariable valueVariable = 
compileStack.defineVariable(loop.getValueVariable(), false);
         Label continueLabel = compileStack.getContinueLabel();
         Label breakLabel = compileStack.getBreakLabel();
 
@@ -210,7 +220,10 @@ public class StaticTypesStatementWriter extends 
StatementWriter {
         mv.visitVarInsn(ALOAD, enumeration);
         ENUMERATION_NEXT_METHOD.call(mv);
         operandStack.push(ClassHelper.OBJECT_TYPE);
-        operandStack.storeVar(variable);
+        operandStack.storeVar(valueVariable);
+        if (indexVariable != null) {
+            mv.visitIincInsn(indexVariable.getIndex(), 1);
+        }
 
         loop.getLoopBlock().visit(controller.getAcg());
         mv.visitJumpInsn(GOTO, continueLabel);
diff --git 
a/src/main/java/org/codehaus/groovy/transform/CategoryASTTransformation.java 
b/src/main/java/org/codehaus/groovy/transform/CategoryASTTransformation.java
index 72662fa825..30249a9a97 100644
--- a/src/main/java/org/codehaus/groovy/transform/CategoryASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/CategoryASTTransformation.java
@@ -221,7 +221,11 @@ public class CategoryASTTransformation implements 
ASTTransformation {
             public void visitForLoop(final ForStatement statement) {
                 Expression exp = statement.getCollectionExpression();
                 exp.visit(this);
-                Parameter loopParam = statement.getVariable();
+                Parameter loopParam = statement.getIndexVariable();
+                if (loopParam != null) {
+                    varStack.getLast().add(loopParam.getName());
+                }
+                loopParam = statement.getValueVariable();
                 if (loopParam != null) {
                     varStack.getLast().add(loopParam.getName());
                 }
diff --git 
a/src/main/java/org/codehaus/groovy/transform/sc/StaticCompilationVisitor.java 
b/src/main/java/org/codehaus/groovy/transform/sc/StaticCompilationVisitor.java
index 893f836a3f..0c806eb4ce 100644
--- 
a/src/main/java/org/codehaus/groovy/transform/sc/StaticCompilationVisitor.java
+++ 
b/src/main/java/org/codehaus/groovy/transform/sc/StaticCompilationVisitor.java
@@ -257,9 +257,10 @@ public class StaticCompilationVisitor extends 
StaticTypeCheckingVisitor {
         super.visitForLoop(statement);
         var collectionExpression = statement.getCollectionExpression();
         if (!(collectionExpression instanceof ClosureListExpression)) {
-            if (statement.getVariable().isDynamicTyped()) { // GROOVY-8169
-                ClassNode inferredType = getType(statement.getVariable());
-                statement.getVariable().setType(inferredType); // GROOVY-5640, 
GROOVY-5641
+            var valueVariable = statement.getValueVariable();
+            if (valueVariable.isDynamicTyped()) { // GROOVY-8169
+                ClassNode inferredType = getType(valueVariable);
+                valueVariable.setType(inferredType); // GROOVY-5640, 
GROOVY-5641
             }
         }
     }
diff --git a/src/spec/test/SemanticsTest.groovy 
b/src/spec/test/SemanticsTest.groovy
index da6a4ca13f..ff2e2b5355 100644
--- a/src/spec/test/SemanticsTest.groovy
+++ b/src/spec/test/SemanticsTest.groovy
@@ -245,6 +245,11 @@ final class SemanticsTest {
             list.add(c)
         }
         assert list == ['a', 'b', 'c']
+
+        // iterate with index
+        for ( int i, k in map.keySet() ) {
+            assert map.get(k) == i + 1
+        }
         // end::groovy_for_loop_example[]
     }
 
diff --git a/src/test/groovy/ForLoopTest.groovy 
b/src/test/groovy/ForLoopTest.groovy
index 5608a553fb..90b3009b03 100644
--- a/src/test/groovy/ForLoopTest.groovy
+++ b/src/test/groovy/ForLoopTest.groovy
@@ -18,6 +18,8 @@
  */
 package groovy
 
+import groovy.transform.CompileStatic
+
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.params.ParameterizedTest
 import org.junit.jupiter.params.provider.ValueSource
@@ -125,6 +127,26 @@ final class ForLoopTest {
         assert answer == [1, 2, 3]
     }
 
+    // GROOVY-10683
+    @Test @CompileStatic
+    void testForEachWithIndex() {
+        for (int i, var v in [0, 1, 2, 3]) {
+            assert i == v
+            continue
+        }
+        for (int i, var v in new int[]{0, 1, 2, 3}) {
+            assert i == v
+            continue
+        }
+        for (int i, var v in Collections.enumeration([0, 1, 2, 3])) {
+            assert i == v
+            continue
+        }
+        for (int i, def ts in Thread.State) {
+            assert i == ((Thread.State) ts).ordinal() // GROOVY-11597
+        }
+    }
+
     @Test
     void testClassicFor() {
         for (int i = 0; i < 10; i++) {
diff --git a/src/test/org/apache/groovy/parser/antlr4/util/AstDumper.groovy 
b/src/test/org/apache/groovy/parser/antlr4/util/AstDumper.groovy
index 48adb6a442..5e9a161599 100644
--- a/src/test/org/apache/groovy/parser/antlr4/util/AstDumper.groovy
+++ b/src/test/org/apache/groovy/parser/antlr4/util/AstDumper.groovy
@@ -533,8 +533,12 @@ class AstNodeToScriptVisitor implements 
CompilationUnit.IPrimaryClassNodeOperati
     void visitForLoop(ForStatement statement) {
         printStatementLabels(statement)
         print 'for ('
-        if (statement?.variable != ForStatement.FOR_LOOP_DUMMY) {
-            visitParameters statement.variable
+        if (statement?.indexVariable) {
+            visitParameters statement.indexVariable
+            print ', '
+        }
+        if (statement?.valueVariable) {
+            visitParameters statement.valueVariable
             print ' : '
         }
 
diff --git 
a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstNodeToScriptAdapter.groovy
 
b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstNodeToScriptAdapter.groovy
index e83dbcdfe7..fc6f2ab327 100644
--- 
a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstNodeToScriptAdapter.groovy
+++ 
b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstNodeToScriptAdapter.groovy
@@ -571,20 +571,19 @@ class AstNodeToScriptVisitor implements 
CompilationUnit.IPrimaryClassNodeOperati
     void visitForLoop(ForStatement statement) {
         printStatementLabels(statement)
         print 'for ('
-        if (statement?.variable != ForStatement.FOR_LOOP_DUMMY) {
-            visitParameters statement.variable
+        if (statement.valueVariable) {
+            if (statement.indexVariable) {
+                visitParameters(statement.indexVariable)
+                print ', '
+            }
+            visitParameters(statement.valueVariable)
             print ' : '
         }
-
-        if (statement?.collectionExpression instanceof ListExpression) {
-            statement?.collectionExpression?.visit this
-        } else {
-            statement?.collectionExpression?.visit this
-        }
+        statement.collectionExpression.visit(this)
         print ') {'
         printLineBreak()
         indented {
-            statement?.loopBlock?.visit this
+            statement.loopBlock.visit(this)
         }
         print '}'
         printLineBreak()

Reply via email to