This is an automated email from the ASF dual-hosted git repository.
emilles pushed a commit to branch GROOVY-7463
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/GROOVY-7463 by this push:
new 41ab74b61b GROOVY-7617: limit label scope
41ab74b61b is described below
commit 41ab74b61b8a9070cace3bcafaa39187580f87e1
Author: Eric Milles <[email protected]>
AuthorDate: Tue Jan 20 09:28:01 2026 -0600
GROOVY-7617: limit label scope
---
.../codehaus/groovy/ast/stmt/ThrowStatement.java | 10 +-
.../org/codehaus/groovy/control/LabelVerifier.java | 179 +++++++++--
.../groovy/groovy/BreakContinueLabelTest.groovy | 336 ++++++++++++---------
3 files changed, 352 insertions(+), 173 deletions(-)
diff --git a/src/main/java/org/codehaus/groovy/ast/stmt/ThrowStatement.java
b/src/main/java/org/codehaus/groovy/ast/stmt/ThrowStatement.java
index ca9632f337..316ba87131 100644
--- a/src/main/java/org/codehaus/groovy/ast/stmt/ThrowStatement.java
+++ b/src/main/java/org/codehaus/groovy/ast/stmt/ThrowStatement.java
@@ -23,7 +23,7 @@ import org.codehaus.groovy.ast.expr.Expression;
/**
- * Represents a throw statement
+ * Represents a throw statement.
*/
public class ThrowStatement extends Statement {
@@ -37,10 +37,6 @@ public class ThrowStatement extends Statement {
return expression;
}
- @Override
- public void visit(GroovyCodeVisitor visitor) {
- visitor.visitThrowStatement(this);
- }
public void setExpression(Expression expression) {
this.expression = expression;
}
@@ -50,4 +46,8 @@ public class ThrowStatement extends Statement {
return "throw " + expression.getText();
}
+ @Override
+ public void visit(GroovyCodeVisitor visitor) {
+ visitor.visitThrowStatement(this);
+ }
}
diff --git a/src/main/java/org/codehaus/groovy/control/LabelVerifier.java
b/src/main/java/org/codehaus/groovy/control/LabelVerifier.java
index 9f53340114..5aa081fe7d 100644
--- a/src/main/java/org/codehaus/groovy/control/LabelVerifier.java
+++ b/src/main/java/org/codehaus/groovy/control/LabelVerifier.java
@@ -19,30 +19,40 @@
package org.codehaus.groovy.control;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
+import org.codehaus.groovy.ast.stmt.AssertStatement;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.BreakStatement;
+import org.codehaus.groovy.ast.stmt.CaseStatement;
+import org.codehaus.groovy.ast.stmt.CatchStatement;
import org.codehaus.groovy.ast.stmt.ContinueStatement;
import org.codehaus.groovy.ast.stmt.DoWhileStatement;
+import org.codehaus.groovy.ast.stmt.EmptyStatement;
+import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.ForStatement;
import org.codehaus.groovy.ast.stmt.IfStatement;
+import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.ast.stmt.SwitchStatement;
+import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
+import org.codehaus.groovy.ast.stmt.ThrowStatement;
+import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
+import java.util.HashMap;
import java.util.HashSet;
-import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
* This class checks the handling of labels in the AST
*/
-public class LabelVerifier extends ClassCodeVisitorSupport {
+public class LabelVerifier extends PrePostStatementVisitor {
boolean inIf, inLoop, inSwitch;
private final SourceUnit source;
- private Set<String> visitedLabels;
- private List<BreakStatement> breakLabels;
- private List<ContinueStatement> continueLabels;
+ private Set<String> availableLabels;
+ private Map<String, Statement> undefinedLabels;
public LabelVerifier(SourceUnit src) {
source = src;
@@ -54,25 +64,25 @@ public class LabelVerifier extends ClassCodeVisitorSupport {
}
protected void assertNoLabelsMissed() {
- // TODO: Report multiple missing labels of the same name only once?
- for (ContinueStatement element : continueLabels) {
- addError("continue to missing label", element);
- }
- for (BreakStatement element : breakLabels) {
- addError("break to missing label", element);
+ for (Map.Entry<String, Statement> entry : undefinedLabels.entrySet()) {
+ String kind = (entry.getValue() instanceof BreakStatement ?
"break" : "continue");
+ addError(String.format("cannot %s to label '%s' from here", kind,
entry.getKey()), entry.getValue());
}
}
@Override
- public void visitStatement(Statement s) {
+ protected void visitStatement(Statement s) {
List<String> labels = s.getStatementLabels();
- if (labels != null && visitedLabels != null) {
- for (String label : labels) {
- visitedLabels.add(label);
+ if (labels != null) {
+ availableLabels.addAll(labels);
+ }
+ }
- breakLabels.removeIf(breakStatement ->
breakStatement.getLabel().equals(label));
- continueLabels.removeIf(continueStatement ->
continueStatement.getLabel().equals(label));
- }
+ @Override
+ protected void postVisitStatement(Statement s) {
+ List<String> labels = s.getStatementLabels();
+ if (labels != null) {
+ availableLabels.removeAll(labels); // GROOVY-7617
}
}
@@ -82,9 +92,8 @@ public class LabelVerifier extends ClassCodeVisitorSupport {
inLoop = false;
inSwitch = false;
- visitedLabels = new HashSet<>();
- breakLabels = new LinkedList<>();
- continueLabels = new LinkedList<>();
+ availableLabels = new HashSet<>();
+ undefinedLabels = new HashMap<>();
super.visitClassCodeContainer(code);
@@ -102,6 +111,7 @@ public class LabelVerifier extends ClassCodeVisitorSupport {
cond.getIfBlock().visit(this);
cond.getElseBlock().visit(this);
inIf = oldInIf;
+ postVisitStatement(cond);
}
@Override
@@ -112,6 +122,7 @@ public class LabelVerifier extends ClassCodeVisitorSupport {
inLoop = true;
loop.getLoopBlock().visit(this);
inLoop = oldInLoop;
+ postVisitStatement(loop);
}
@Override
@@ -122,6 +133,7 @@ public class LabelVerifier extends ClassCodeVisitorSupport {
inLoop = true;
loop.getLoopBlock().visit(this);
inLoop = oldInLoop;
+ postVisitStatement(loop);
}
@Override
@@ -132,6 +144,7 @@ public class LabelVerifier extends ClassCodeVisitorSupport {
loop.getLoopBlock().visit(this);
inLoop = oldInLoop;
loop.getBooleanExpression().visit(this);
+ postVisitStatement(loop);
}
@Override
@@ -143,6 +156,7 @@ public class LabelVerifier extends ClassCodeVisitorSupport {
switchStatement.getCaseStatements().forEach(this::visit);
switchStatement.getDefaultStatement().visit(this);
inSwitch = oldInSwitch;
+ postVisitStatement(switchStatement);
}
@Override
@@ -156,11 +170,10 @@ public class LabelVerifier extends
ClassCodeVisitorSupport {
if (!inLoop && !inIf) { // GROOVY-7463
addError("the break statement with named label is only allowed
inside control statements", breakStatement);
}
- if (!visitedLabels.contains(label)) {
- breakLabels.add(breakStatement);
+ if (!availableLabels.contains(label)) {
+ undefinedLabels.put(label, breakStatement);
}
}
- super.visitBreakStatement(breakStatement);
}
@Override
@@ -170,10 +183,122 @@ public class LabelVerifier extends
ClassCodeVisitorSupport {
}
String label = continueStatement.getLabel();
if (label != null) {
- if (!visitedLabels.contains(label)) {
- continueLabels.add(continueStatement);
+ if (!availableLabels.contains(label)) {
+ undefinedLabels.put(label, continueStatement);
}
}
- super.visitContinueStatement(continueStatement);
+ }
+}
+
+//------------------------------------------------------------------------------
+
+abstract class PrePostStatementVisitor extends ClassCodeVisitorSupport {
+
+ @Override
+ protected abstract void visitStatement(Statement statement);
+
+ protected abstract void postVisitStatement(Statement statement);
+
+ @Override
+ public void visitAssertStatement(AssertStatement statement) {
+ super.visitAssertStatement(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitBlockStatement(BlockStatement statement) {
+ super.visitBlockStatement(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitBreakStatement(BreakStatement statement) {
+ super.visitBreakStatement(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitCaseStatement(CaseStatement statement) {
+ super.visitCaseStatement(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitCatchStatement(CatchStatement statement) {
+ super.visitCatchStatement(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitContinueStatement(ContinueStatement statement) {
+ super.visitContinueStatement(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitDoWhileLoop(DoWhileStatement statement) {
+ super.visitDoWhileLoop(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitEmptyStatement(EmptyStatement statement) {
+ visitStatement(statement); // TODO: Move to super?
+ super.visitEmptyStatement(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitExpressionStatement(ExpressionStatement statement) {
+ super.visitExpressionStatement(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitForLoop(ForStatement statement) {
+ super.visitForLoop(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitIfElse(IfStatement statement) {
+ super.visitIfElse(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitReturnStatement(ReturnStatement statement) {
+ super.visitReturnStatement(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitSwitch(SwitchStatement statement) {
+ super.visitSwitch(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitSynchronizedStatement(SynchronizedStatement statement) {
+ super.visitSynchronizedStatement(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitThrowStatement(ThrowStatement statement) {
+ super.visitThrowStatement(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitTryCatchFinally(TryCatchStatement statement) {
+ super.visitTryCatchFinally(statement);
+ postVisitStatement(statement);
+ }
+
+ @Override
+ public void visitWhileLoop(WhileStatement statement) {
+ super.visitWhileLoop(statement);
+ postVisitStatement(statement);
}
}
diff --git a/src/test/groovy/groovy/BreakContinueLabelTest.groovy
b/src/test/groovy/groovy/BreakContinueLabelTest.groovy
index 5788aaba65..a865e009ea 100644
--- a/src/test/groovy/groovy/BreakContinueLabelTest.groovy
+++ b/src/test/groovy/groovy/BreakContinueLabelTest.groovy
@@ -20,192 +20,240 @@ package groovy
import org.junit.jupiter.api.Test
+import static groovy.test.GroovyAssert.assertScript
import static groovy.test.GroovyAssert.shouldFail
-import static org.junit.jupiter.api.Assertions.fail
final class BreakContinueLabelTest {
@Test
void testDeclareSimpleLabels() {
- label_1: print('foo')
- label_2:
- print('bar')
+ assertScript '''
+ label_1: print('foo')
+ label_2:
+ print('bar')
+ '''
}
// GROOVY-7463
@Test
void testBreakLabelInIfStatement() {
- boolean flag = true
- label:
- if (flag) {
- print('foo')
+ assertScript '''
+ boolean flag = true
+ label:
if (flag) {
- break label
- fail()
+ print('foo')
+ if (flag) {
+ break label
+ assert false
+ }
+ assert false
+ } else {
+ assert false
}
- fail()
- } else {
- fail()
- }
- print('bar')
+ print('bar')
+ '''
}
// GROOVY-7463
@Test
void testBreakLabelInIfStatement2() {
- int i = 0
- boolean flag = true
- label:
- if (flag) {
- print('foo')
- try {
- if (flag) {
- print('bar')
- break label
- fail()
+ assertScript '''
+ int i = 0
+ boolean flag = true
+ label:
+ if (flag) {
+ print('foo')
+ try {
+ if (flag) {
+ print('bar')
+ break label;
+ assert false
+ }
+ assert false
+ } finally {
+ i += 1
}
- fail()
- } finally {
- i += 1
+ assert false
}
- fail()
- }
- print('baz')
- assert i == 1
+ print('baz')
+ assert i == 1
+ '''
}
@Test
void testBreakLabelInSimpleForLoop() {
- label: for (i in [1]) {
- break label;
- assert false
- }
+ assertScript '''
+ label: for (i in [1]) {
+ break label;
+ assert false
+ }
+ '''
+ }
+
+ // GROOVY-7617
+ @Test
+ void testBreakLabelInSecondForLoop() {
+ def err = shouldFail '''
+ @groovy.transform.TimedInterrupt(1)
+ void test() {
+ first:
+ for (i in [1]) {
+ break first // okay
+ assert false
+ }
+ for (j in [2]) {
+ break first // fail
+ assert false
+ }
+ }
+
+ test()
+ '''
+ assert err =~ /cannot break to label 'first' from here/
}
@Test
void testBreakLabelInNestedForLoop() {
- label: for (i in [1]) {
- for (j in [1]) {
- break label;
- assert false : 'did not break inner loop'
+ assertScript '''
+ label: for (i in [1]) {
+ for (j in [1]) {
+ break label;
+ assert false : 'did not break inner loop'
+ }
+ assert false : 'did not break outer loop'
}
- assert false : 'did not break outer loop'
- }
+ '''
}
@Test
void testBreakLabelInForLoopTryFinally() {
- int i = 0
- out:
- for (j in 1..2) {
- try {
+ assertScript '''
+ int i = 0
+ out:
+ for (j in 1..2) {
try {
- break out
+ try {
+ break out
+ assert false
+ } finally {
+ i += 10
+ }
assert false
} finally {
- i += 10
+ i += 1
}
assert false
- } finally {
- i += 1
}
- assert false
- }
- assert i == 11
+ assert i == 11
+ '''
}
@Test
void testBreakInNestedForLoop() {
- def reached = false
- for (i in [1]) {
- for (j in [1]) {
- break
- assert false : 'did not break inner loop'
+ assertScript '''
+ def reached = false
+ for (i in [1]) {
+ for (j in [1]) {
+ break
+ assert false : 'did not break inner loop'
+ }
+ reached = true
}
- reached = true
- }
- assert reached : 'must not break outer loop'
+ assert reached : 'must not break outer loop'
+ '''
}
@Test
void testBreakLabelInSimpleWhileLoop() {
- label_1: while (true) {
- break label_1
- assert false
- }
+ assertScript '''
+ label: while (true) {
+ break label;
+ assert false
+ }
+ '''
}
@Test
void testBreakLabelInNestedWhileLoop() {
- def count = 0
- label: while (count < 1) {
- count += 1
- while (true) {
- break label
- assert false : 'did not break inner loop'
+ assertScript '''
+ def count = 0
+ label: while (count < 1) {
+ count += 1
+ while (true) {
+ break label
+ assert false : 'did not break inner loop'
+ }
+ assert false : 'did not break outer loop'
}
- assert false : 'did not break outer loop'
- }
+ '''
}
@Test
void testBreakLabelInNestedMixedForAndWhileLoop() {
- def count = 0
- label_1: while (count < 1) {
- count += 1
- for (i in [1]) {
- break label_1
- assert false : 'did not break inner loop'
+ assertScript '''
+ def count = 0
+ label_1: while (count < 1) {
+ count += 1
+ for (i in [1]) {
+ break label_1
+ assert false : 'did not break inner loop'
+ }
+ assert false : 'did not break outer loop'
}
- assert false : 'did not break outer loop'
- }
- label_2: for (i in [1]) {
- while (true) {
- break label_2
- assert false : 'did not break inner loop'
+ label_2: for (i in [1]) {
+ while (true) {
+ break label_2
+ assert false : 'did not break inner loop'
+ }
+ assert false : 'did not break outer loop'
}
- assert false : 'did not break outer loop'
- }
+ '''
}
// GROOVY-11739
@Test
void testContinueWithinDoWhileLoop() {
- int i = 0
- do {
- i += 1
- if (i > 1000) break // prevent infinite loop
- continue // control should pass to condition
- } while (i < 100)
+ assertScript '''
+ int i = 0
+ do {
+ i += 1
+ if (i > 1000) break // prevent infinite loop
+ continue // control should pass to condition
+ } while (i < 100)
- assert i == 100
+ assert i == 100
+ '''
}
@Test
void testContinueInNestedForLoop() {
- def log = ''
- for (i in [1,2]) {
- log += i
- for (j in [3,4]) {
- if (j==3) continue
- log += j
+ assertScript '''
+ def log = ''
+ for (i in [1,2]) {
+ log += i
+ for (j in [3,4]) {
+ if (j==3) continue
+ log += j
+ }
}
- }
- assert log == '1424'
+ assert log == '1424'
+ '''
}
@Test
void testContinueLabelInNestedForLoop() {
- def log = ''
- label: for (i in [1,2]) {
- log += i
- for (j in [3,4]) {
- if (j==4) continue label
- log += j
+ assertScript '''
+ def log = ''
+ label: for (i in [1,2]) {
+ log += i
+ for (j in [3,4]) {
+ if (j==4) continue label
+ log += j
+ }
+ log += 'never reached'
}
- log += 'never reached'
- }
- assert log == '1323'
+ assert log == '1323'
+ '''
}
// GROOVY-3908
@@ -229,44 +277,50 @@ final class BreakContinueLabelTest {
@Test
void testBreakToLastLabelSucceeds() {
- one:
- two:
- three:
- for (i in 1..2) {
- break three
- fail()
- }
+ assertScript '''
+ one:
+ two:
+ three:
+ for (i in 1..2) {
+ break three
+ assert false
+ }
+ '''
}
@Test
void testMultipleLabelSupport() {
- def visited = []
- label1:
- label2:
- label3:
- for (int i = 0; i < 9; i++) {
- visited << i
- if (i == 1) continue label1
- visited << 10 + i
- if (i == 3) continue label2
- visited << 100 + i
- if (i == 5) break label3
- }
- assert visited == [0, 10, 100, 1, 2, 12, 102, 3, 13, 4, 14, 104, 5,
15, 105]
+ assertScript '''
+ def visited = []
+ label1:
+ label2:
+ label3:
+ for (int i = 0; i < 9; i++) {
+ visited << i
+ if (i == 1) continue label1
+ visited << 10 + i
+ if (i == 3) continue label2
+ visited << 100 + i
+ if (i == 5) break label3
+ }
+ assert visited == [0, 10, 100, 1, 2, 12, 102, 3, 13, 4, 14, 104,
5, 15, 105]
+ '''
}
// this is in accordance with Java; Spock Framework relies on this
@Test
void testLabelCanOccurMultipleTimesInSameScope() {
- one:
- for (i in 1..2) {
- break one
- fail()
- }
- one:
- for (i in 1..2) {
- break one
- fail()
- }
+ assertScript '''
+ one:
+ for (i in 1..2) {
+ break one
+ assert false
+ }
+ one:
+ for (i in 1..2) {
+ break one
+ assert false
+ }
+ '''
}
}