You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/05/17 07:47:30 UTC
(groovy) branch GROOVY-10156 updated (1cafcc02c4 -> 06dcecf784)
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a change to branch GROOVY-10156
in repository https://gitbox.apache.org/repos/asf/groovy.git
omit 1cafcc02c4 GROOVY-10156: Unreachable bytecode in switch statement
add c90bcc3587 minor refactor: try to fix sonar reporting
add bdf5045262 minor refactor: fix project key
add e31361dfc2 minor refactor: fix project key
add 2c070143e9 GROOVY-12010: Graduate groovy-toml from incubating to stable
add 9fca0f8624 Bump github/codeql-action from 4.35.3 to 4.35.4 (#2540)
add 151a4c749c Bump testlens-app/setup-testlens from 1.9.0 to 1.9.2 (#2539)
add 9ab4e19a94 AI readiness: make it clearer around selecting java and groovy versions
add 1c20a0f307 AI readiness: backport changes from magpie learnings
add dec7cf5a77 AI readiness: backport changes from magpie learnings
add 7f4fb8afd4 AI readiness: sandboxed reproducers
new 06dcecf784 GROOVY-10156: Unreachable bytecode in switch statement
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (1cafcc02c4)
\
N -- N -- N refs/heads/GROOVY-10156 (06dcecf784)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.agents/skills/groovy-build/SKILL.md | 44 +++-
.agents/skills/groovy-fix-workflow/SKILL.md | 45 +++-
.agents/skills/groovy-internals/SKILL.md | 6 +-
.agents/skills/groovy-jira/SKILL.md | 26 ++
.agents/skills/groovy-jira/jira-issue.sh | 87 +++++++
.agents/skills/groovy-jira/jira-search.sh | 59 +++++
.agents/skills/groovy-reassess/SKILL.md | 128 +++++++++-
.agents/skills/groovy-reassess/dashboard.groovy | 281 +++++++++++++++++++++
.agents/skills/groovy-reproducer/SKILL.md | 216 ++++++++++++++--
.../groovy-reproducer/run-reproducer-sandboxed.sh | 143 +++++++++++
.../skills/groovy-reproducer/safety-prescreen.sh | 63 +++++
.agents/skills/groovy-skills/SKILL.md | 22 +-
.agents/skills/groovy-triage/SKILL.md | 39 ++-
.github/workflows/codeql-analysis.yml | 6 +-
.github/workflows/groovy-build-coverage.yml | 4 +-
.github/workflows/groovy-build-test.yml | 2 +-
AGENTS.md | 86 +++++++
ARCHITECTURE.md | 16 ++
CONTRIBUTING.md | 82 +++++-
README.adoc | 2 +
build.gradle | 1 +
subprojects/groovy-toml/build.gradle | 1 +
.../src/main/java/groovy/toml/TomlBuilder.java | 3 +-
.../src/main/java/groovy/toml/TomlSlurper.java | 16 +-
.../groovy-toml/src/spec/doc/toml-userguide.adoc | 31 ++-
.../spec/test/groovy/toml/TomlParserTest.groovy | 57 +++++
versions.properties | 1 +
27 files changed, 1418 insertions(+), 49 deletions(-)
create mode 100755 .agents/skills/groovy-jira/jira-issue.sh
create mode 100755 .agents/skills/groovy-jira/jira-search.sh
create mode 100644 .agents/skills/groovy-reassess/dashboard.groovy
create mode 100755 .agents/skills/groovy-reproducer/run-reproducer-sandboxed.sh
create mode 100755 .agents/skills/groovy-reproducer/safety-prescreen.sh
(groovy) 01/01: GROOVY-10156: Unreachable bytecode in switch statement
Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch GROOVY-10156
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 06dcecf78460da67a296ccd5dabb79192407f153
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sun May 17 13:32:12 2026 +0900
GROOVY-10156: Unreachable bytecode in switch statement
---
.../codehaus/groovy/ast/tools/GeneralUtils.java | 367 ++++++++++-
.../codehaus/groovy/classgen/asm/CompileStack.java | 48 +-
.../groovy/classgen/asm/StatementWriter.java | 215 ++++---
.../asm/sc/StaticTypesStatementWriter.java | 17 +-
src/test/groovy/bugs/Groovy10156.groovy | 669 +++++++++++++++++++++
.../classgen/asm/AbstractBytecodeTestCase.groovy | 65 +-
6 files changed, 1255 insertions(+), 126 deletions(-)
diff --git a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
index 05086ea028..483a204afa 100644
--- a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
+++ b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
@@ -19,6 +19,7 @@
package org.codehaus.groovy.ast.tools;
import groovy.lang.MetaProperty;
+import groovy.transform.Internal;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
@@ -38,10 +39,12 @@ import org.codehaus.groovy.ast.expr.BooleanExpression;
import org.codehaus.groovy.ast.expr.CastExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.ClosureListExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.ElvisOperatorExpression;
+import org.codehaus.groovy.ast.expr.EmptyExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.FieldExpression;
import org.codehaus.groovy.ast.expr.LambdaExpression;
@@ -56,18 +59,23 @@ import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
import org.codehaus.groovy.ast.expr.TernaryExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
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.LoopingStatement;
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 org.codehaus.groovy.classgen.BytecodeExpression;
import org.codehaus.groovy.control.io.ReaderSource;
import org.codehaus.groovy.runtime.GeneratedClosure;
@@ -1264,26 +1272,359 @@ public class GeneralUtils {
return false;
} else if (statement instanceof ThrowStatement) {
return false;
- } else if (statement instanceof BlockStatement) {
- List<Statement> list = ((BlockStatement) statement).getStatements();
+ } else if (statement instanceof BlockStatement blockStmt) {
+ List<Statement> list = blockStmt.getStatements();
final int last = list.size() - 1;
if (!maybeFallsThrough(list.get(last))) return false;
for (int i = 0; i < last; i += 1)
if (!maybeFallsThrough(list.get(i))) return false;
- } else if (statement instanceof IfStatement) {
- return maybeFallsThrough(((IfStatement) statement).getElseBlock())
- || maybeFallsThrough(((IfStatement) statement).getIfBlock());
- } else if (statement instanceof SwitchStatement) {
- // TODO
- } else if (statement instanceof TryCatchStatement tryCatch) {
- if (!maybeFallsThrough(tryCatch.getFinallyStatement())) return false;
- for (CatchStatement cs : tryCatch.getCatchStatements())
+ } else if (statement instanceof IfStatement ifStmt) {
+ return maybeFallsThrough(ifStmt.getElseBlock())
+ || maybeFallsThrough(ifStmt.getIfBlock());
+ } else if (statement instanceof LoopingStatement loopingStmt) {
+ return maybeFallsThroughLoop(loopingStmt);
+ } else if (statement instanceof SwitchStatement switchStmt) {
+ return maybeFallsThroughSwitch(switchStmt);
+ } else if (statement instanceof TryCatchStatement tryCatchStmt) {
+ if (!maybeFallsThrough(tryCatchStmt.getFinallyStatement())) return false;
+ for (CatchStatement cs : tryCatchStmt.getCatchStatements())
if (maybeFallsThrough(cs.getCode())) return true;
- return maybeFallsThrough(tryCatch.getTryStatement());
- } else if (statement instanceof SynchronizedStatement) {
- return maybeFallsThrough(((SynchronizedStatement) statement).getCode());
+ return maybeFallsThrough(tryCatchStmt.getTryStatement());
+ } else if (statement instanceof SynchronizedStatement syncStmt) {
+ return maybeFallsThrough(syncStmt.getCode());
}
return true;
}
+
+ /**
+ * Returns {@code true} if the given statement is effectively empty — i.e. it is {@code null},
+ * an {@link EmptyStatement}, or a {@link BlockStatement} whose every nested statement is itself
+ * empty (recursively). This is subtly different from {@link Statement#isEmpty()}, which only
+ * checks whether a {@link BlockStatement} has zero entries, not whether those entries are all
+ * empty.
+ */
+ @Internal
+ public static boolean isEmptyStatement(final Statement statement) {
+ if (statement == null || statement.isEmpty()) return true;
+ if (statement instanceof BlockStatement blockStatement) {
+ for (Statement subStatement : blockStatement.getStatements()) {
+ if (!isEmptyStatement(subStatement)) return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the compile-time constant boolean value of a loop condition expression.
+ * Handles nested {@link BooleanExpression} / {@link NotExpression} wrappers and
+ * an absent (empty) condition, which is always {@code true} (e.g. {@code for(;;)}).
+ * Returns {@link ConditionValue#UNKNOWN} when the value cannot be determined statically.
+ */
+ @Internal
+ public static ConditionValue constantBooleanValue(Expression expression) {
+ if (expression instanceof EmptyExpression) {
+ return ConditionValue.ALWAYS_TRUE; // empty for-loop condition is always true (e.g. for(;;))
+ }
+ boolean reverse = false;
+ while (expression instanceof BooleanExpression boolExpr) {
+ if (boolExpr instanceof NotExpression) reverse = !reverse;
+ expression = boolExpr.getExpression();
+ }
+ if (expression instanceof ConstantExpression ce && ce.getValue() instanceof Boolean) {
+ return ((Boolean) ce.getValue()) != reverse ? ConditionValue.ALWAYS_TRUE : ConditionValue.ALWAYS_FALSE;
+ }
+ return ConditionValue.UNKNOWN;
+ }
+
+ /**
+ * Indicates whether switch-case code may fall through into the next case body.
+ */
+ @Internal
+ public static boolean maybeFallsThroughToNextSwitchCase(final Statement statement, final SwitchStatement switchStatement) {
+ Objects.requireNonNull(switchStatement);
+ return analyzeStatementFlow(statement).mayContinue;
+ }
+
+ private static boolean maybeFallsThroughSwitch(final SwitchStatement statement) {
+ return analyzeStatementFlow(statement).mayContinue;
+ }
+
+ private static boolean maybeFallsThroughLoop(final LoopingStatement statement) {
+ return analyzeLoopFlow(statement).mayContinue;
+ }
+
+ /**
+ * Indicates whether execution of a loop body may reach the loop's condition/update step.
+ */
+ @Internal
+ public static boolean mayReachLoopCondition(final LoopingStatement statement) {
+ StatementFlow flow = analyzeStatementFlow(statement.getLoopBlock());
+ Set<String> loopLabels = labelSet(((Statement) statement).getStatementLabels());
+ return mayReachLoopCondition(flow, loopLabels);
+ }
+
+ private static boolean mayReachLoopCondition(final StatementFlow flow, final Set<String> loopLabels) {
+ return flow.mayContinue || flow.mayContinueUnlabeled || flow.continuesToAny(loopLabels);
+ }
+
+ private static boolean mayBreakTo(final StatementFlow flow, final Set<String> labels) {
+ return flow.mayBreakUnlabeled || flow.breaksToAny(labels);
+ }
+
+ private static StatementFlow analyzeStatementFlow(final Statement statement) {
+ if (statement.isEmpty()) return StatementFlow.FALL_THROUGH;
+
+ if (statement instanceof BreakStatement breakStatement) {
+ return StatementFlow.breakFlow(breakStatement.getLabel());
+ } else if (statement instanceof ContinueStatement continueStatement) {
+ return StatementFlow.continueFlow(continueStatement.getLabel());
+ } else if (statement instanceof ReturnStatement || statement instanceof ThrowStatement) {
+ return StatementFlow.ABRUPT;
+ } else if (statement instanceof BlockStatement block) {
+ StatementFlow flow = StatementFlow.FALL_THROUGH;
+ for (Statement subStatement : block.getStatements()) {
+ flow = flow.then(analyzeStatementFlow(subStatement));
+ if (!flow.mayContinue) break;
+ }
+ return flow.consumeLabels(labelSet(block.getStatementLabels()));
+ } else if (statement instanceof IfStatement ifStatement) {
+ StatementFlow thenFlow = analyzeStatementFlow(ifStatement.getIfBlock());
+ StatementFlow elseFlow = analyzeStatementFlow(ifStatement.getElseBlock());
+ return thenFlow.or(elseFlow);
+ } else if (statement instanceof TryCatchStatement tryCatch) {
+ StatementFlow finallyFlow = analyzeStatementFlow(tryCatch.getFinallyStatement());
+ StatementFlow bodyFlow = analyzeStatementFlow(tryCatch.getTryStatement());
+ for (CatchStatement catchStatement : tryCatch.getCatchStatements()) {
+ bodyFlow = bodyFlow.or(analyzeStatementFlow(catchStatement.getCode()));
+ }
+ return bodyFlow.thenFinally(finallyFlow);
+ } else if (statement instanceof SynchronizedStatement synchronizedStatement) {
+ return analyzeStatementFlow(synchronizedStatement.getCode());
+ } else if (statement instanceof LoopingStatement loopingStatement) {
+ return analyzeLoopFlow(loopingStatement);
+ } else if (statement instanceof SwitchStatement switchStatement) {
+ return analyzeNestedSwitchFlow(switchStatement);
+ }
+
+ return StatementFlow.of(maybeFallsThrough(statement), Set.of(), Set.of());
+ }
+
+ private static StatementFlow analyzeLoopFlow(final LoopingStatement statement) {
+ StatementFlow flow = analyzeStatementFlow(statement.getLoopBlock());
+ ConditionValue condition = loopCondition(statement);
+ Set<String> loopLabels = labelSet(((Statement) statement).getStatementLabels());
+ boolean mayBreakOutOfLoop = mayBreakTo(flow, loopLabels);
+ boolean mayReachLoopCondition = mayReachLoopCondition(flow, loopLabels);
+ boolean mayExitViaCondition = mayExitLoopViaCondition(statement, condition, mayReachLoopCondition);
+ boolean mayRunLoopBlock = mayRunLoopBlock(statement, condition);
+ Set<String> breakLabels = mayRunLoopBlock ? withoutLabels(flow.breakLabels, loopLabels) : Set.of();
+ Set<String> continueLabels = mayRunLoopBlock ? withoutLabels(flow.continueLabels, loopLabels) : Set.of();
+ return StatementFlow.of(mayBreakOutOfLoop || mayExitViaCondition, breakLabels, continueLabels);
+ }
+
+ private static boolean mayRunLoopBlock(final LoopingStatement statement, final ConditionValue condition) {
+ return !(statement instanceof WhileStatement || isClassicForLoop(statement))
+ || condition != ConditionValue.ALWAYS_FALSE;
+ }
+
+ private static boolean mayExitLoopViaCondition(final LoopingStatement statement, final ConditionValue condition, final boolean bodyMayContinue) {
+ if (statement instanceof DoWhileStatement) {
+ return bodyMayContinue && condition != ConditionValue.ALWAYS_TRUE;
+ }
+ if (statement instanceof ForStatement forStatement && !isClassicForLoop(forStatement)) {
+ return true;
+ }
+ return condition != ConditionValue.ALWAYS_TRUE;
+ }
+
+ private static boolean isClassicForLoop(final LoopingStatement statement) {
+ return statement instanceof ForStatement
+ && ((ForStatement) statement).getCollectionExpression() instanceof ClosureListExpression;
+ }
+
+ private static ConditionValue loopCondition(final LoopingStatement statement) {
+ if (statement instanceof WhileStatement whileStatement) {
+ return constantBooleanValue(whileStatement.getBooleanExpression());
+ }
+ if (statement instanceof DoWhileStatement doWhileStatement) {
+ return constantBooleanValue(doWhileStatement.getBooleanExpression());
+ }
+ if (statement instanceof ForStatement forStatement
+ && forStatement.getCollectionExpression() instanceof ClosureListExpression closureListExpression) {
+ List<Expression> expressions = closureListExpression.getExpressions();
+ if (!expressions.isEmpty()) {
+ return constantBooleanValue(expressions.get((expressions.size() - 1) / 2));
+ }
+ }
+ return ConditionValue.UNKNOWN;
+ }
+
+ private static Set<String> labelSet(final List<String> labels) {
+ if (labels == null || labels.isEmpty()) return Set.of();
+ return new LinkedHashSet<>(labels);
+ }
+
+ private static StatementFlow analyzeNestedSwitchFlow(final SwitchStatement statement) {
+ Set<String> switchLabels = labelSet(statement.getStatementLabels());
+ StatementFlow nextFlow = flowAfterSwitch(statement.getDefaultStatement(), switchLabels);
+ StatementFlow switchFlow = nextFlow;
+
+ List<CaseStatement> caseStatements = statement.getCaseStatements();
+ for (int i = caseStatements.size() - 1; i >= 0; i -= 1) {
+ StatementFlow flow = analyzeStatementFlow(caseStatements.get(i).getCode());
+ boolean mayBreakSwitch = mayBreakTo(flow, switchLabels);
+ Set<String> breakLabels = withoutLabels(flow.breakLabels, switchLabels);
+ nextFlow = StatementFlow.of(
+ mayBreakSwitch || (flow.mayContinue && nextFlow.mayContinue),
+ union(breakLabels, flow.mayContinue ? nextFlow.breakLabels : Set.of()),
+ union(flow.continueLabels, flow.mayContinue ? nextFlow.continueLabels : Set.of())
+ );
+ switchFlow = nextFlow.or(switchFlow);
+ }
+
+ return switchFlow;
+ }
+
+ private static StatementFlow flowAfterSwitch(final Statement statement, final Set<String> switchLabels) {
+ StatementFlow flow = analyzeStatementFlow(statement);
+ return StatementFlow.of(
+ mayBreakTo(flow, switchLabels) || flow.mayContinue,
+ withoutLabels(flow.breakLabels, switchLabels),
+ flow.continueLabels
+ );
+ }
+
+ private static Set<String> union(final Set<String> left, final Set<String> right) {
+ if (left.isEmpty()) return right;
+ if (right.isEmpty()) return left;
+
+ Set<String> labels = new LinkedHashSet<>(left);
+ labels.addAll(right);
+ return labels;
+ }
+
+ private static Set<String> withoutLabels(final Set<String> labels, final Set<String> removedLabels) {
+ if (labels.isEmpty() || removedLabels.isEmpty()) return labels;
+
+ Set<String> remainingLabels = new LinkedHashSet<>(labels);
+ remainingLabels.removeAll(removedLabels);
+ return remainingLabels;
+ }
+
+ /**
+ * Represents the flow of control through a switch case or loop body, tracking whether execution may continue past the end of the block
+ * @param mayContinue whether execution may continue past the end of the block (i.e. not all paths return, throw, or break out of the block)
+ * @param mayBreakUnlabeled whether the block may be exited via an unlabeled break statement
+ * @param breakLabels the set of labels of break statements that may exit the block (if any)
+ */
+ private record StatementFlow(boolean mayContinue, boolean mayBreakUnlabeled, Set<String> breakLabels,
+ boolean mayContinueUnlabeled, Set<String> continueLabels) {
+ /**
+ * Code always terminates abruptly via {@code return}, {@code throw}, or {@code continue}.
+ */
+ static final StatementFlow ABRUPT = new StatementFlow(false, false, Set.of(), false, Set.of());
+ /**
+ * Code may continue after this statement and does not leave unresolved breaks behind.
+ */
+ static final StatementFlow FALL_THROUGH = new StatementFlow(true, false, Set.of(), false, Set.of());
+
+ static StatementFlow breakFlow(final String label) {
+ if (label == null) return flow(false, true, Set.of(), false, Set.of());
+ return flow(false, false, Set.of(label), false, Set.of());
+ }
+
+ static StatementFlow continueFlow(final String label) {
+ if (label == null) return flow(false, false, Set.of(), true, Set.of());
+ return flow(false, false, Set.of(), false, Set.of(label));
+ }
+
+ static StatementFlow of(final boolean mayContinue, final Set<String> breakLabels, final Set<String> continueLabels) {
+ return flow(mayContinue, false, breakLabels, false, continueLabels);
+ }
+
+ private static StatementFlow flow(final boolean mayContinue, final boolean mayBreakUnlabeled, final Set<String> breakLabels,
+ final boolean mayContinueUnlabeled, final Set<String> continueLabels) {
+ if (!mayContinue && !mayBreakUnlabeled && breakLabels.isEmpty() && !mayContinueUnlabeled && continueLabels.isEmpty()) {
+ return ABRUPT;
+ }
+ if (mayContinue && !mayBreakUnlabeled && breakLabels.isEmpty() && !mayContinueUnlabeled && continueLabels.isEmpty()) {
+ return FALL_THROUGH;
+ }
+ return new StatementFlow(mayContinue, mayBreakUnlabeled, breakLabels, mayContinueUnlabeled, continueLabels);
+ }
+
+ StatementFlow then(final StatementFlow next) {
+ if (!mayContinue) return this;
+ if (!mayBreakUnlabeled && breakLabels.isEmpty() && !mayContinueUnlabeled && continueLabels.isEmpty()) return next;
+ return flow(
+ next.mayContinue,
+ mayBreakUnlabeled || next.mayBreakUnlabeled,
+ union(breakLabels, next.breakLabels),
+ mayContinueUnlabeled || next.mayContinueUnlabeled,
+ union(continueLabels, next.continueLabels)
+ );
+ }
+
+ StatementFlow or(final StatementFlow other) {
+ return flow(
+ mayContinue || other.mayContinue,
+ mayBreakUnlabeled || other.mayBreakUnlabeled,
+ union(breakLabels, other.breakLabels),
+ mayContinueUnlabeled || other.mayContinueUnlabeled,
+ union(continueLabels, other.continueLabels)
+ );
+ }
+
+ StatementFlow thenFinally(final StatementFlow finallyFlow) {
+ return flow(
+ finallyFlow.mayContinue && mayContinue,
+ finallyFlow.mayBreakUnlabeled || (finallyFlow.mayContinue && mayBreakUnlabeled),
+ union(finallyFlow.breakLabels, finallyFlow.mayContinue ? breakLabels : Set.of()),
+ finallyFlow.mayContinueUnlabeled || (finallyFlow.mayContinue && mayContinueUnlabeled),
+ union(finallyFlow.continueLabels, finallyFlow.mayContinue ? continueLabels : Set.of())
+ );
+ }
+
+ StatementFlow consumeLabels(final Set<String> labels) {
+ if (labels.isEmpty() || breakLabels.isEmpty()) return this;
+
+ Set<String> remainingBreakLabels = withoutLabels(breakLabels, labels);
+ boolean consumedBreak = remainingBreakLabels.size() != breakLabels.size();
+ return flow(mayContinue || consumedBreak, mayBreakUnlabeled, remainingBreakLabels, mayContinueUnlabeled, continueLabels);
+ }
+
+ boolean breaksToAny(final Set<String> labels) {
+ if (labels.isEmpty() || breakLabels.isEmpty()) return false;
+
+ for (String label : labels) {
+ if (breakLabels.contains(label)) return true;
+ }
+ return false;
+ }
+
+ boolean continuesToAny(final Set<String> labels) {
+ if (labels.isEmpty() || continueLabels.isEmpty()) return false;
+
+ for (String label : labels) {
+ if (continueLabels.contains(label)) return true;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Represents the compile-time constant value of a boolean loop-condition or branch expression.
+ * Used by flow analysis to avoid emitting unreachable bytecode.
+ */
+ @Internal
+ public enum ConditionValue {
+ /** The boolean expression always evaluates to {@code false}. */
+ ALWAYS_FALSE,
+ /** The boolean expression always evaluates to {@code true}. */
+ ALWAYS_TRUE,
+ /** The value cannot be determined at compile time. */
+ UNKNOWN
+ }
}
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/CompileStack.java b/src/main/java/org/codehaus/groovy/classgen/asm/CompileStack.java
index 314e7ef91e..737f5a3851 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/CompileStack.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/CompileStack.java
@@ -554,13 +554,7 @@ public class CompileStack {
pushVariableScope(scope);
continueLabel = new Label();
breakLabel = new Label();
- if (labelNames != null) {
- for (String labelName: labelNames) {
- if (labelName == null) continue;
- namedLoopBreakLabel.put(labelName, breakLabel);
- namedLoopContinueLabel.put(labelName, continueLabel);
- }
- }
+ registerNamedLabels(labelNames, breakLabel, continueLabel);
}
/**
@@ -582,13 +576,7 @@ public class CompileStack {
pushState();
continueLabel = new Label();
breakLabel = new Label();
- if (labelNames != null) {
- for (String labelName: labelNames) {
- if (labelName == null) continue;
- namedLoopBreakLabel.put(labelName, breakLabel);
- namedLoopContinueLabel.put(labelName, continueLabel);
- }
- }
+ registerNamedLabels(labelNames, breakLabel, continueLabel);
}
/**
@@ -607,8 +595,23 @@ public class CompileStack {
* @return the break label
*/
public Label pushSwitch() {
+ return pushSwitch(null);
+ }
+
+ /**
+ * Creates a new break label, registers it as the named break target for each
+ * label name, and pushes an element onto the state stack so that a later call
+ * to {@link #pop()} restores the previous state.
+ *
+ * @param labelNames the statement labels on the switch (may be {@code null})
+ * @return the break label
+ *
+ * @since 6.0.0
+ */
+ public Label pushSwitch(final List<String> labelNames) {
pushState();
breakLabel = new Label();
+ registerNamedLabels(labelNames, breakLabel, null);
return breakLabel;
}
@@ -623,13 +626,22 @@ public class CompileStack {
public Label pushBreakable(final List<String> labelNames) {
pushState();
Label namedBreakLabel = new Label();
- if (labelNames != null) {
- for (String labelName: labelNames) {
- if (labelName == null) continue;
+ registerNamedLabels(labelNames, namedBreakLabel, null);
+ return namedBreakLabel;
+ }
+
+ private void registerNamedLabels(final List<String> labelNames, final Label namedBreakLabel, final Label namedContinueLabel) {
+ if (labelNames == null) return;
+
+ for (String labelName : labelNames) {
+ if (labelName == null) continue;
+ if (namedBreakLabel != null) {
namedLoopBreakLabel.put(labelName, namedBreakLabel);
}
+ if (namedContinueLabel != null) {
+ namedLoopContinueLabel.put(labelName, namedContinueLabel);
+ }
}
- return namedBreakLabel;
}
/**
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 9faca8a624..3c2655ca48 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/StatementWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/StatementWriter.java
@@ -22,14 +22,11 @@ import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.expr.BinaryExpression;
-import org.codehaus.groovy.ast.expr.BooleanExpression;
import org.codehaus.groovy.ast.expr.ClosureListExpression;
-import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.EmptyExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCall;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
-import org.codehaus.groovy.ast.expr.NotExpression;
import org.codehaus.groovy.ast.stmt.AssertStatement;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.BreakStatement;
@@ -37,7 +34,6 @@ 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;
@@ -59,8 +55,28 @@ import java.util.Optional;
import java.util.function.Consumer;
import static org.apache.groovy.ast.tools.ExpressionUtils.isNullConstant;
-import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
-import static org.objectweb.asm.Opcodes.*;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ConditionValue;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.castX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.constantBooleanValue;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.isEmptyStatement;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.mayReachLoopCondition;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.maybeFallsThrough;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.maybeFallsThroughToNextSwitchCase;
+import static org.objectweb.asm.Opcodes.ALOAD;
+import static org.objectweb.asm.Opcodes.ATHROW;
+import static org.objectweb.asm.Opcodes.CHECKCAST;
+import static org.objectweb.asm.Opcodes.GOTO;
+import static org.objectweb.asm.Opcodes.IADD;
+import static org.objectweb.asm.Opcodes.ICONST_1;
+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.INVOKEVIRTUAL;
+import static org.objectweb.asm.Opcodes.MONITORENTER;
+import static org.objectweb.asm.Opcodes.MONITOREXIT;
+import static org.objectweb.asm.Opcodes.NOP;
+import static org.objectweb.asm.Opcodes.RETURN;
/**
* Generates bytecode for Groovy statements by visiting AST statement nodes
@@ -187,15 +203,13 @@ public class StatementWriter {
OperandStack operandStack = controller.getOperandStack();
// 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 indexVariable = defineLoopIndexVariable(statement);
BytecodeVariable valueVariable = compileStack.defineVariable(statement.getValueVariable(), false);
// get the iterator and generate the loop control
int iterator = compileStack.defineTemporaryVariable("iterator", ClassHelper.Iterator_TYPE, true);
Label breakLabel = compileStack.getBreakLabel(), continueLabel = compileStack.getContinueLabel();
+ boolean bodyMayReachContinue = mayReachLoopCondition(statement);
mv.visitVarInsn(ALOAD, iterator);
mv.visitJumpInsn(IFNULL, breakLabel);
@@ -227,12 +241,26 @@ public class StatementWriter {
// generate the loop body
statement.getLoopBlock().visit(controller.getAcg());
- mv.visitJumpInsn(GOTO, continueLabel);
+ writeLoopBackEdge(continueLabel, bodyMayReachContinue);
mv.visitLabel(breakLabel);
compileStack.removeVar(iterator);
}
+ protected final BytecodeVariable defineLoopIndexVariable(final ForStatement statement) {
+ CompileStack compileStack = controller.getCompileStack();
+ return Optional.ofNullable(statement.getIndexVariable()).map(iv -> {
+ controller.getMethodVisitor().visitInsn(ICONST_M1); // initialize to -1 so increment can pair with next()
+ return compileStack.defineVariable(iv, true);
+ }).orElse(null);
+ }
+
+ protected final void writeLoopBackEdge(final Label continueLabel, final boolean bodyMayReachContinue) {
+ if (bodyMayReachContinue) {
+ controller.getMethodVisitor().visitJumpInsn(GOTO, continueLabel);
+ }
+ }
+
/**
* Emits the {@link java.util.Iterator#hasNext()} call via the given visitor.
* Overrideable so subclasses can substitute a specialized or inlined variant.
@@ -287,31 +315,26 @@ public class StatementWriter {
Label cond = new Label();
mv.visitLabel(cond);
- // visit condition leave boolean on stack
- {
- int mark = controller.getOperandStack().getStackLength();
- Expression condExpr = expressions.get(condIndex);
- condExpr.visit(controller.getAcg());
- controller.getOperandStack().castToBool(mark, true);
- }
- // jump if we don't want to continue
- // note: ifeq tests for ==0, a boolean is 0 if it is false
- controller.getOperandStack().jump(IFEQ, breakLabel);
-
- // Generate the loop body
- statement.getLoopBlock().visit(controller.getAcg());
-
- // visit increment
- mv.visitLabel(continueLabel);
- // fix for being on the wrong line when debugging for loop
- controller.getAcg().onLineNumber(statement, "increment condition");
- for (int i = condIndex + 1; i < size; i += 1) {
- visitExpressionOfLoopStatement(expressions.get(i));
+ ConditionValue conditionValue = visitConditionOfLoopStatement(expressions.get(condIndex), breakLabel, mv);
+ boolean bodyMayReachContinue = mayReachLoopCondition(statement);
+ if (conditionValue != ConditionValue.ALWAYS_FALSE) {
+ // Generate the loop body
+ statement.getLoopBlock().visit(controller.getAcg());
+
+ if (bodyMayReachContinue) {
+ // visit increment
+ mv.visitLabel(continueLabel);
+ // fix for being on the wrong line when debugging for loop
+ controller.getAcg().onLineNumber(statement, "increment condition");
+ for (int i = condIndex + 1; i < size; i += 1) {
+ visitExpressionOfLoopStatement(expressions.get(i));
+ }
+
+ // jump to test the condition again
+ writeLoopBackEdge(cond, true);
+ }
}
- // jump to test the condition again
- mv.visitJumpInsn(GOTO, cond);
-
// loop end
mv.visitLabel(breakLabel);
@@ -334,27 +357,21 @@ public class StatementWriter {
}
}
- private void visitConditionOfLoopingStatement(final BooleanExpression expression, final Label breakLabel, final MethodVisitor mv) {
- Expression expr = expression;
- boolean reverse = false;
- do { // undo arbitrary nesting of (Boolean|Not)Expressions
- if (expr instanceof NotExpression) reverse = !reverse;
- expr = ((BooleanExpression) expr).getExpression();
- } while (expr instanceof BooleanExpression);
-
- // optimize constant boolean condition
- if (expr instanceof ConstantExpression && ((ConstantExpression) expr).getValue() instanceof Boolean) {
- if (((ConstantExpression) expr).isFalseExpression() && !reverse) {
- mv.visitJumpInsn(GOTO, breakLabel); // unconditional exit
- return;
- } else {
- // unconditional loop
- return;
- }
+ private ConditionValue visitConditionOfLoopStatement(final Expression expression, final Label breakLabel, final MethodVisitor mv) {
+ ConditionValue conditionValue = constantBooleanValue(expression);
+ if (conditionValue == ConditionValue.ALWAYS_FALSE) {
+ mv.visitJumpInsn(GOTO, breakLabel); // unconditional exit
+ return conditionValue;
+ }
+ if (conditionValue == ConditionValue.ALWAYS_TRUE) {
+ return conditionValue; // unconditional loop
}
+ int mark = controller.getOperandStack().getStackLength();
expression.visit(controller.getAcg());
+ controller.getOperandStack().castToBool(mark, true);
controller.getOperandStack().jump(IFEQ, breakLabel);
+ return ConditionValue.UNKNOWN;
}
/**
@@ -370,14 +387,15 @@ public class StatementWriter {
compileStack.pushLoop(statement.getStatementLabels());
Label continueLabel = compileStack.getContinueLabel();
Label breakLabel = compileStack.getBreakLabel();
+ boolean bodyMayReachContinue = mayReachLoopCondition(statement);
MethodVisitor mv = controller.getMethodVisitor();
mv.visitLabel(continueLabel);
- visitConditionOfLoopingStatement(statement.getBooleanExpression(), breakLabel, mv);
- statement.getLoopBlock().visit(controller.getAcg());
-
- mv.visitJumpInsn(GOTO, continueLabel);
+ if (visitConditionOfLoopStatement(statement.getBooleanExpression(), breakLabel, mv) != ConditionValue.ALWAYS_FALSE) {
+ statement.getLoopBlock().visit(controller.getAcg());
+ writeLoopBackEdge(continueLabel, bodyMayReachContinue);
+ }
mv.visitLabel(breakLabel);
compileStack.pop();
@@ -401,10 +419,12 @@ public class StatementWriter {
mv.visitLabel(blockLabel);
statement.getLoopBlock().visit(controller.getAcg());
- mv.visitLabel(continueLabel); // GROOVY-11739: continue jumps to condition
- visitConditionOfLoopingStatement(statement.getBooleanExpression(), breakLabel, mv);
-
- mv.visitJumpInsn(GOTO, blockLabel);
+ if (mayReachLoopCondition(statement)) {
+ mv.visitLabel(continueLabel); // GROOVY-11739: continue jumps to condition
+ if (visitConditionOfLoopStatement(statement.getBooleanExpression(), breakLabel, mv) != ConditionValue.ALWAYS_FALSE) {
+ mv.visitJumpInsn(GOTO, blockLabel);
+ }
+ }
mv.visitLabel(breakLabel);
compileStack.pop();
@@ -453,6 +473,10 @@ public class StatementWriter {
Statement tryStatement = statement.getTryStatement();
Statement finallyStatement = statement.getFinallyStatement();
+ if (isEmptyStatement(finallyStatement)) {
+ writeTryCatch(statement, tryStatement, mv, compileStack);
+ return;
+ }
BlockRecorder tryBlock = makeBlockRecorder(finallyStatement);
startRange(tryBlock, mv);
@@ -529,12 +553,54 @@ public class StatementWriter {
compileStack.removeVar(anyThrowable);
}
+ private void writeTryCatch(final TryCatchStatement statement, final Statement tryStatement, final MethodVisitor mv, final CompileStack compileStack) {
+ List<CatchStatement> catchStatements = statement.getCatchStatements();
+ if (catchStatements.isEmpty()) {
+ tryStatement.visit(controller.getAcg());
+ return;
+ }
+
+ Label tryStart = new Label();
+ Label tryEnd = new Label();
+ Label afterCatch = new Label();
+ boolean fallthrough = false;
+
+ mv.visitLabel(tryStart);
+ tryStatement.visit(controller.getAcg());
+ if (maybeFallsThrough(tryStatement)) {
+ mv.visitJumpInsn(GOTO, afterCatch);
+ fallthrough = true;
+ }
+ mv.visitLabel(tryEnd);
+
+ for (CatchStatement catchStatement : catchStatements) {
+ Label catchBlock = new Label();
+ String typeName = BytecodeHelper.getClassInternalName(catchStatement.getExceptionType());
+ compileStack.addExceptionBlock(tryStart, tryEnd, catchBlock, typeName);
+ mv.visitLabel(catchBlock);
+
+ compileStack.pushState();
+ compileStack.defineVariable(catchStatement.getVariable(), catchStatement.getExceptionType(), true);
+ catchStatement.visit(controller.getAcg());
+ compileStack.pop();
+
+ if (maybeFallsThrough(catchStatement.getCode())) {
+ mv.visitJumpInsn(GOTO, afterCatch);
+ fallthrough = true;
+ }
+ }
+
+ if (fallthrough) {
+ mv.visitLabel(afterCatch);
+ }
+ }
+
private BlockRecorder makeBlockRecorder(final Statement finallyStatement) {
BlockRecorder recorder = new BlockRecorder();
final CompileStack compileStack = controller.getCompileStack();
recorder.excludedStatement = () -> {
- if (finallyStatement == null || finallyStatement instanceof EmptyStatement) return;
+ if (isEmptyStatement(finallyStatement)) return;
final VariableScope originalScope = compileStack.getScope();
compileStack.pop();
@@ -578,7 +644,7 @@ public class StatementWriter {
// switch does not have a continue label; use enclosing continue label
CompileStack compileStack = controller.getCompileStack();
- Label breakLabel = compileStack.pushSwitch();
+ Label breakLabel = compileStack.pushSwitch(statement.getStatementLabels());
int switchVariableIndex = compileStack.defineTemporaryVariable("switch", exprType, true);
@@ -591,24 +657,26 @@ public class StatementWriter {
int i = 0;
for (Iterator<CaseStatement> iter = caseStatements.iterator(); iter.hasNext(); i += 1) {
- writeCaseStatement(iter.next(), switchVariableIndex, labels[i], labels[i + 1]);
+ writeCaseStatement(iter.next(), statement, switchVariableIndex, labels[i], labels[i + 1]);
}
statement.getDefaultStatement().visit(controller.getAcg());
- controller.getMethodVisitor().visitLabel(breakLabel);
+ if (maybeFallsThrough(statement) || statement.getStatementLabels() != null) {
+ controller.getMethodVisitor().visitLabel(breakLabel);
+ }
compileStack.removeVar(switchVariableIndex);
compileStack.pop();
}
- private void writeCaseStatement(final CaseStatement statement, final int switchVariableIndex, final Label thisLabel, final Label nextLabel) {
- controller.getAcg().onLineNumber(statement, "visitCaseStatement");
+ private void writeCaseStatement(final CaseStatement caseStatement, final SwitchStatement switchStatement, final int switchVariableIndex, final Label thisLabel, final Label nextLabel) {
+ controller.getAcg().onLineNumber(caseStatement, "visitCaseStatement");
MethodVisitor mv = controller.getMethodVisitor();
OperandStack operandStack = controller.getOperandStack();
mv.visitVarInsn(ALOAD, switchVariableIndex);
- statement.getExpression().visit(controller.getAcg());
+ caseStatement.getExpression().visit(controller.getAcg());
operandStack.box();
controller.getBinaryExpressionHelper().getIsCaseMethod().call(mv);
operandStack.replace(ClassHelper.boolean_TYPE);
@@ -617,10 +685,10 @@ public class StatementWriter {
mv.visitLabel(thisLabel);
- statement.getCode().visit(controller.getAcg());
+ caseStatement.getCode().visit(controller.getAcg());
// now if we don't finish with a break we need to jump past the next comparison
- if (nextLabel != null) {
+ if (nextLabel != null && maybeFallsThroughToNextSwitchCase(caseStatement.getCode(), switchStatement)) {
mv.visitJumpInsn(GOTO, nextLabel);
}
@@ -699,13 +767,18 @@ public class StatementWriter {
compileStack.writeExceptionTable(fb, catchAll, null);
compileStack.pop(); //pop fb
- finallyPart.run();
- mv.visitJumpInsn(GOTO, synchronizedEnd);
+ boolean fallthrough = maybeFallsThrough(statement.getCode());
+ if (fallthrough) {
+ finallyPart.run();
+ mv.visitJumpInsn(GOTO, synchronizedEnd);
+ }
mv.visitLabel(catchAll);
finallyPart.run();
mv.visitInsn(ATHROW);
- mv.visitLabel(synchronizedEnd);
+ if (fallthrough) {
+ mv.visitLabel(synchronizedEnd);
+ }
compileStack.removeVar(index);
}
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 573ff1616b..01e1dea81d 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,7 +38,6 @@ 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;
@@ -48,10 +47,8 @@ import static org.objectweb.asm.Opcodes.CALOAD;
import static org.objectweb.asm.Opcodes.DALOAD;
import static org.objectweb.asm.Opcodes.DUP;
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;
@@ -119,12 +116,11 @@ public class StaticTypesStatementWriter extends StatementWriter {
MethodVisitor mv = controller.getMethodVisitor();
AsmClassGenerator acg = controller.getAcg();
- BytecodeVariable indexVariable = Optional.ofNullable(loop.getIndexVariable()).map(iv -> {
- mv.visitInsn(ICONST_M1); return compileStack.defineVariable(iv, true);
- }).orElse(null);
+ BytecodeVariable indexVariable = defineLoopIndexVariable(loop);
BytecodeVariable valueVariable = compileStack.defineVariable(loop.getValueVariable(), arrayType.getComponentType(), false);
Label continueLabel = compileStack.getContinueLabel();
Label breakLabel = compileStack.getBreakLabel();
+ boolean bodyMayReachContinue = GeneralUtils.mayReachLoopCondition(loop);
// load array on stack
arrayExpression.visit(acg);
@@ -161,7 +157,7 @@ public class StaticTypesStatementWriter extends StatementWriter {
// loop body
loop.getLoopBlock().visit(acg);
- mv.visitJumpInsn(GOTO, continueLabel);
+ writeLoopBackEdge(continueLabel, bodyMayReachContinue);
mv.visitLabel(breakLabel);
@@ -202,12 +198,11 @@ public class StaticTypesStatementWriter extends StatementWriter {
OperandStack operandStack = controller.getOperandStack();
MethodVisitor mv = controller.getMethodVisitor();
- BytecodeVariable indexVariable = Optional.ofNullable(loop.getIndexVariable()).map(iv -> {
- mv.visitInsn(ICONST_M1); return compileStack.defineVariable(iv, true);
- }).orElse(null);
+ BytecodeVariable indexVariable = defineLoopIndexVariable(loop);
BytecodeVariable valueVariable = compileStack.defineVariable(loop.getValueVariable(), false);
Label continueLabel = compileStack.getContinueLabel();
Label breakLabel = compileStack.getBreakLabel();
+ boolean bodyMayReachContinue = GeneralUtils.mayReachLoopCondition(loop);
collectionExpression.visit(controller.getAcg());
@@ -231,7 +226,7 @@ public class StaticTypesStatementWriter extends StatementWriter {
}
loop.getLoopBlock().visit(controller.getAcg());
- mv.visitJumpInsn(GOTO, continueLabel);
+ writeLoopBackEdge(continueLabel, bodyMayReachContinue);
mv.visitLabel(breakLabel);
}
diff --git a/src/test/groovy/bugs/Groovy10156.groovy b/src/test/groovy/bugs/Groovy10156.groovy
new file mode 100644
index 0000000000..d9c945c05d
--- /dev/null
+++ b/src/test/groovy/bugs/Groovy10156.groovy
@@ -0,0 +1,669 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package bugs
+
+import org.codehaus.groovy.classgen.asm.AbstractBytecodeTestCase
+import org.junit.jupiter.api.Test
+
+import java.lang.reflect.Array
+
+final class Groovy10156 extends AbstractBytecodeTestCase {
+
+ @Test
+ void testEnumSwitchWithBreakDoesNotEmitSyntheticFallthroughJumps() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e) {
+ int result = 0
+ switch (e) {
+ case E.ONE:
+ result = 1
+ break
+ case E.TWO:
+ result = 2
+ break
+ case E.THREE:
+ result = 3
+ break
+ default:
+ result = 4
+ }
+ result
+ }
+ ''', 'ONE, TWO, THREE')
+ }
+
+ @Test
+ void testEnumSwitchWithReturnDoesNotEmitSyntheticFallthroughJumps() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ return 1
+ case E.TWO:
+ return 2
+ default:
+ return 3
+ }
+ }
+ ''')
+ }
+
+ @Test
+ void testEnumSwitchWithThrowDoesNotEmitSyntheticFallthroughJumps() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ throw new IllegalStateException('one')
+ case E.TWO:
+ throw new IllegalStateException('two')
+ default:
+ throw new IllegalStateException('default')
+ }
+ }
+ ''')
+ }
+
+ @Test
+ void testEnumSwitchWithNestedSwitchWhereAllPathsReturnDoesNotEmitSyntheticFallthroughJumps() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e, boolean flag) {
+ switch (e) {
+ case E.ONE:
+ switch (flag) {
+ case true:
+ return 1
+ default:
+ return 2
+ }
+ case E.TWO:
+ return 3
+ default:
+ return 4
+ }
+ }
+ ''')
+ }
+
+ @Test
+ void testEnumSwitchWithContinueDoesNotEmitSyntheticFallthroughJumps() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E[] values) {
+ int result = 0
+ outer:
+ for (E e : values) {
+ switch (e) {
+ case E.ONE:
+ result += 1
+ continue outer
+ case E.TWO:
+ result += 2
+ break
+ default:
+ result += 4
+ }
+ result += 8
+ }
+ result
+ }
+ ''', 'ONE, TWO, THREE')
+ }
+
+ @Test
+ void testEnumSwitchWithTryCatchFinallyWhereAllPathsReturnDoesNotEmitSyntheticFallthroughJumps() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ try {
+ return 1
+ } catch (RuntimeException ignored) {
+ return 2
+ } finally {
+ }
+ default:
+ return 3
+ }
+ }
+ ''', 'ONE')
+ }
+
+ @Test
+ void testEnumSwitchWithSynchronizedReturnDoesNotEmitSyntheticFallthroughJumps() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ synchronized (this) {
+ return 1
+ }
+ default:
+ return 2
+ }
+ }
+ ''')
+ }
+
+ @Test
+ void testEnumSwitchWithInfiniteWhileDoesNotEmitSyntheticFallthroughJumps() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ while (true) {
+ }
+ case E.TWO:
+ return 2
+ default:
+ return 3
+ }
+ }
+ ''')
+ }
+
+ @Test
+ void testEnumSwitchWithInfiniteDoWhileDoesNotEmitSyntheticFallthroughJumps() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ do {
+ } while (true)
+ case E.TWO:
+ return 2
+ default:
+ return 3
+ }
+ }
+ ''')
+ }
+
+ @Test
+ void testEnumSwitchWithConstantFalseWhileStillFallsThrough() {
+ assertNoUnreachableBytecodeAndRunEnumSwitchAssertions('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ while (false) {
+ return 99
+ }
+ case E.TWO:
+ return 2
+ default:
+ return 3
+ }
+ }
+ ''', 'ONE, TWO, THREE') { loader ->
+ def c = newTestSubject(loader)
+ assert c.test(enumValue(loader, 'ONE')) == 2
+ assert c.test(enumValue(loader, 'TWO')) == 2
+ assert c.test(enumValue(loader, 'THREE')) == 3
+ }
+ }
+
+ @Test
+ void testEnumSwitchWithClassicForConstantFalseStillFallsThrough() {
+ assertNoUnreachableBytecodeAndRunEnumSwitchAssertions('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ for (; false ;) {
+ return 99
+ }
+ case E.TWO:
+ return 2
+ default:
+ return 3
+ }
+ }
+ ''', 'ONE, TWO, THREE') { loader ->
+ def c = newTestSubject(loader)
+ assert c.test(enumValue(loader, 'ONE')) == 2
+ assert c.test(enumValue(loader, 'TWO')) == 2
+ assert c.test(enumValue(loader, 'THREE')) == 3
+ }
+ }
+
+ @Test
+ void testEnumSwitchWithForInLoopStillFallsThrough() {
+ assertNoUnreachableBytecodeAndRunEnumSwitchAssertions('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ for (value in [] as List<Integer>) {
+ return 99
+ }
+ case E.TWO:
+ return 2
+ default:
+ return 3
+ }
+ }
+ ''', 'ONE, TWO, THREE') { loader ->
+ def c = newTestSubject(loader)
+ assert c.test(enumValue(loader, 'ONE')) == 2
+ assert c.test(enumValue(loader, 'TWO')) == 2
+ assert c.test(enumValue(loader, 'THREE')) == 3
+ }
+ }
+
+ @Test
+ void testEnumSwitchWithInfiniteClassicForDoesNotEmitSyntheticFallthroughJumps() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ for (;;) {
+ return 1
+ }
+ default:
+ return 2
+ }
+ }
+ ''')
+ }
+
+ @Test
+ void testEnumSwitchWithEmptyCaseStillFallsThrough() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ case E.TWO:
+ return 2
+ default:
+ return 3
+ }
+ }
+ ''')
+ }
+
+ @Test
+ void testEnumSwitchWithMixedBreakAndFallThroughPreservesSemantics() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e, boolean stopEarly) {
+ int result = 0
+ switch (e) {
+ case E.ONE:
+ if (stopEarly) break
+ result = 1
+ case E.TWO:
+ return result + 2
+ default:
+ return 4
+ }
+ return result + 8
+ }
+ ''', 'ONE, TWO, THREE')
+ }
+
+ @Test
+ void testLabeledSwitchRemainsValid() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e) {
+ outer:
+ switch (e) {
+ case E.ONE:
+ return 1
+ default:
+ return 2
+ }
+ }
+ ''')
+ }
+
+ @Test
+ void testNestedLabeledSwitchRemainsValid() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e, boolean flag) {
+ switch (e) {
+ case E.ONE:
+ inner:
+ switch (flag) {
+ case true:
+ return 2
+ default:
+ return 1
+ }
+ default:
+ return 3
+ }
+ }
+ ''')
+ }
+
+ @Test
+ void testSwitchFlowAnalysisHandlesLabeledBlockBreaksInCaseBodies() {
+ assertNoUnreachableBytecodeAndRunEnumSwitchAssertions('''
+ int test(E e, boolean stopEarly) {
+ switch (e) {
+ case E.ONE:
+ inner: {
+ if (stopEarly) {
+ break inner
+ }
+ return 1
+ }
+ case E.TWO:
+ return 2
+ default:
+ return 3
+ }
+ }
+ ''', 'ONE, TWO, THREE') { loader ->
+ def c = newTestSubject(loader)
+ assert c.test(enumValue(loader, 'ONE'), false) == 1
+ assert c.test(enumValue(loader, 'ONE'), true) == 2
+ assert c.test(enumValue(loader, 'TWO'), false) == 2
+ assert c.test(enumValue(loader, 'THREE'), false) == 3
+ }
+ }
+
+ @Test
+ void testSwitchFlowAnalysisHandlesTryCatchFinallyInCaseBodies() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e, boolean flag) {
+ switch (e) {
+ case E.ONE:
+ try {
+ if (flag) return 1
+ } catch (RuntimeException ignored) {
+ return 2
+ } finally {
+ }
+ case E.TWO:
+ return 3
+ default:
+ return 4
+ }
+ }
+ ''')
+ }
+
+ @Test
+ void testSwitchFlowAnalysisHandlesTryFinallyWithoutCatch() {
+ assertNoUnreachableBytecodeAndRunEnumSwitchAssertions('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ try {
+ return 1
+ } finally {
+ }
+ default:
+ return 2
+ }
+ }
+ ''') { loader ->
+ def c = newTestSubject(loader)
+ assert c.test(enumValue(loader, 'ONE')) == 1
+ assert c.test(enumValue(loader, 'TWO')) == 2
+ }
+ }
+
+ @Test
+ void testSwitchFlowAnalysisHandlesTryCatchFallthroughInCaseBodies() {
+ assertNoUnreachableBytecodeAndRunEnumSwitchAssertions('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ try {
+ throw new RuntimeException('boom')
+ } catch (RuntimeException ignored) {
+ } finally {
+ }
+ case E.TWO:
+ return 2
+ default:
+ return 3
+ }
+ }
+ ''') { loader ->
+ def c = newTestSubject(loader)
+ assert c.test(enumValue(loader, 'ONE')) == 2
+ assert c.test(enumValue(loader, 'TWO')) == 2
+ }
+ }
+
+ @Test
+ void testSwitchFlowAnalysisHandlesSynchronizedBlocksInCaseBodies() {
+ assertNoUnreachableBytecodeInEnumSwitchMethod('''
+ int test(E e, boolean flag) {
+ switch (e) {
+ case E.ONE:
+ synchronized (this) {
+ if (flag) {
+ return 1
+ }
+ }
+ case E.TWO:
+ return 2
+ default:
+ return 3
+ }
+ }
+ ''')
+ }
+
+ @Test
+ void testSwitchFlowAnalysisHandlesInfiniteWhileWithBreakInCaseBodies() {
+ assertRunEnumSwitchAssertions('''
+ int test(E e, boolean stopEarly) {
+ switch (e) {
+ case E.ONE:
+ while (true) {
+ if (stopEarly) break
+ return 1
+ }
+ case E.TWO:
+ return 2
+ default:
+ return 3
+ }
+ }
+ ''', 'ONE, TWO, THREE') { loader ->
+ def c = newTestSubject(loader)
+ assert c.test(enumValue(loader, 'ONE'), false) == 1
+ assert c.test(enumValue(loader, 'ONE'), true) == 2
+ assert c.test(enumValue(loader, 'TWO'), false) == 2
+ assert c.test(enumValue(loader, 'THREE'), false) == 3
+ }
+ }
+
+ @Test
+ void testLabeledBreakToSwitchInsideLoopStillAllowsNormalCompletion() {
+ assertNoUnreachableBytecodeAndRunEnumSwitchAssertions('''
+ int test(E e) {
+ int result = 0
+ synchronized (this) {
+ outer:
+ switch (e) {
+ case E.ONE:
+ for (int i = 0; i < 3; i++) {
+ if (i == 1) break outer
+ }
+ return 99
+ default:
+ return -1
+ }
+ result = 7
+ }
+ return result
+ }
+ ''') { loader ->
+ def c = newTestSubject(loader)
+ assert c.test(enumValue(loader, 'ONE')) == 7
+ assert c.test(enumValue(loader, 'TWO')) == -1
+ }
+ }
+
+ @Test
+ void testEnumSwitchWithDoWhileContinueStillFallsThrough() {
+ assertNoUnreachableBytecodeAndRunEnumSwitchAssertions('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ do {
+ continue
+ } while (false)
+ case E.TWO:
+ return 2
+ default:
+ return 3
+ }
+ }
+ ''', 'ONE, TWO, THREE') { loader ->
+ def c = newTestSubject(loader)
+ assert c.test(enumValue(loader, 'ONE')) == 2
+ assert c.test(enumValue(loader, 'TWO')) == 2
+ assert c.test(enumValue(loader, 'THREE')) == 3
+ }
+ }
+
+ @Test
+ void testEnumSwitchWithLabeledDoWhileContinueStillFallsThrough() {
+ assertNoUnreachableBytecodeAndRunEnumSwitchAssertions('''
+ int test(E e) {
+ switch (e) {
+ case E.ONE:
+ loop:
+ do {
+ continue loop
+ } while (false)
+ case E.TWO:
+ return 2
+ default:
+ return 3
+ }
+ }
+ ''', 'ONE, TWO, THREE') { loader ->
+ def c = newTestSubject(loader)
+ assert c.test(enumValue(loader, 'ONE')) == 2
+ assert c.test(enumValue(loader, 'TWO')) == 2
+ assert c.test(enumValue(loader, 'THREE')) == 3
+ }
+ }
+
+ @Test
+ void testSwitchFlowAnalysisKeepsOuterLabeledContinueAbrupt() {
+ assertNoUnreachableBytecodeAndRunEnumSwitchAssertions('''
+ int test(E[] values) {
+ int result = 0
+ outer:
+ for (E e : values) {
+ switch (e) {
+ case E.ONE:
+ do {
+ result += 1
+ continue outer
+ } while (false)
+ default:
+ result += 10
+ }
+ result += 100
+ }
+ return result
+ }
+ ''') { loader ->
+ def c = newTestSubject(loader)
+ def enumType = loadEnumType(loader)
+ def pair = Array.newInstance(enumType, 2)
+ Array.set(pair, 0, enumValue(loader, 'ONE'))
+ Array.set(pair, 1, enumValue(loader, 'TWO'))
+ def single = Array.newInstance(enumType, 1)
+ Array.set(single, 0, enumValue(loader, 'ONE'))
+ assert c.test(pair) == 111
+ assert c.test(single) == 1
+ }
+ }
+
+ private void assertNoUnreachableBytecodeInTestMethod(final String source) {
+ def unreachable = compileAndFindUnreachableInstructions(classNamePattern: 'C', method: 'test', source)
+ assert unreachable.isEmpty(): "Unexpected unreachable instructions: ${unreachable}\n${sequence}"
+ }
+
+ private void assertNoUnreachableBytecodeAndRunAssertions(final String source, final Closure<?> assertions) {
+ assertNoUnreachableBytecodeInTestMethod(source)
+ assertRunAssertions(source, assertions)
+ }
+
+ private void assertNoUnreachableBytecodeInEnumSwitchMethod(final String methodSource, final String enumConstants = 'ONE, TWO') {
+ assertNoUnreachableBytecodeInTestMethod(enumSwitchScript(methodSource, enumConstants))
+ }
+
+ private void assertNoUnreachableBytecodeAndRunEnumSwitchAssertions(final String methodSource, final Closure<?> assertions) {
+ assertNoUnreachableBytecodeAndRunEnumSwitchAssertions(methodSource, 'ONE, TWO', assertions)
+ }
+
+ private void assertNoUnreachableBytecodeAndRunEnumSwitchAssertions(final String methodSource, final String enumConstants, final Closure<?> assertions) {
+ assertNoUnreachableBytecodeAndRunAssertions(enumSwitchScript(methodSource, enumConstants), assertions)
+ }
+
+ private void assertRunEnumSwitchAssertions(final String methodSource, final Closure<?> assertions) {
+ assertRunEnumSwitchAssertions(methodSource, 'ONE, TWO', assertions)
+ }
+
+ private void assertRunEnumSwitchAssertions(final String methodSource, final String enumConstants, final Closure<?> assertions) {
+ assertRunAssertions(enumSwitchScript(methodSource, enumConstants), assertions)
+ }
+
+ private void assertRunAssertions(final String source, final Closure<?> assertions) {
+ def loader = new GroovyClassLoader(this.class.classLoader)
+ try {
+ loader.parseClass(source, 'Groovy10156Runtime.groovy')
+ assertions.call(loader)
+ } finally {
+ loader.close()
+ }
+ }
+
+ private static String enumSwitchScript(final String methodSource, final String enumConstants = 'ONE, TWO') {
+ """
+ import groovy.transform.CompileStatic
+
+ @CompileStatic
+ enum E {
+ ${enumConstants}
+ }
+
+ @CompileStatic
+ class C {
+ ${indent(methodSource.stripIndent().trim())}
+ }
+ """.stripIndent()
+ }
+
+ private static String indent(final String source, final int spaces = 4) {
+ def prefix = ' ' * spaces
+ source.readLines().collect { line -> line ? prefix + line : line }.join('\n')
+ }
+
+ private static Object newTestSubject(final GroovyClassLoader loader) {
+ loader.loadClass('C').getDeclaredConstructor().newInstance()
+ }
+
+ private static Class<?> loadEnumType(final GroovyClassLoader loader) {
+ loader.loadClass('E')
+ }
+
+ private static Object enumValue(final GroovyClassLoader loader, final String constantName) {
+ Enum.valueOf(loadEnumType(loader), constantName)
+ }
+
+}
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 482b59db87..e2ee450506 100644
--- a/src/test/groovy/org/codehaus/groovy/classgen/asm/AbstractBytecodeTestCase.groovy
+++ b/src/test/groovy/org/codehaus/groovy/classgen/asm/AbstractBytecodeTestCase.groovy
@@ -27,6 +27,10 @@ import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.FieldVisitor
import org.objectweb.asm.MethodVisitor
+import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.analysis.Analyzer
+import org.objectweb.asm.tree.analysis.BasicInterpreter
+import org.objectweb.asm.util.Printer
import org.objectweb.asm.util.TraceClassVisitor
import java.security.CodeSource
@@ -37,11 +41,13 @@ import java.security.CodeSource
abstract class AbstractBytecodeTestCase {
Class clazz
+ byte[] classBytes
Map extractionOptions
InstructionSequence sequence
@BeforeEach
void setUp() {
+ classBytes = null
extractionOptions = [method: 'run']
}
@@ -59,7 +65,7 @@ abstract class AbstractBytecodeTestCase {
if (unit != null) {
try {
def firstClass = unit.classes.find { it.name == unit.firstClassNode.name }
- sequence = extractSequence(firstClass.bytes, extractionOptions)
+ captureClassBytesAndSequence(firstClass, extractionOptions)
if (extractionOptions.print) println(sequence)
} catch (e) {
// probably an error in the script
@@ -78,6 +84,7 @@ abstract class AbstractBytecodeTestCase {
options = [method: 'run', classNamePattern: '.*script', *: options]
sequence = null
clazz = null
+ classBytes = null
def cu = new CompilationUnit()
def su = cu.addSource('script', scriptText)
cu.compile(Phases.CONVERSION)
@@ -88,12 +95,12 @@ abstract class AbstractBytecodeTestCase {
for (gc in cu.classes) {
if (gc.name ==~ options.classNamePattern) {
- sequence = extractSequence(gc.bytes, options)
+ captureClassBytesAndSequence(gc, options)
}
}
if (sequence == null && cu.classes) {
def gc = cu.classes.find { it.name == cu.firstClassNode.name }
- sequence = extractSequence(gc.bytes, options)
+ captureClassBytesAndSequence(gc, options)
}
for (gc in cu.classes) {
try {
@@ -117,24 +124,16 @@ abstract class AbstractBytecodeTestCase {
@Override
MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String... exceptions) {
if (options.method == name) {
- // last in "tcv.p.text" is a list that will be filled by "super.visit"
- tcv.p.text.add(tcv.p.text.size() - 2, '--BEGIN--\n')
- try {
+ return withSelectionMarkers(tcv) {
super.visitMethod(access, name, desc, signature, exceptions)
- } finally {
- tcv.p.text.add('--END--\n')
}
}
}
@Override
FieldVisitor visitField(final int access, final String name, final String desc, final String signature, final Object value) {
if (options.field == name) {
- // last in "tcv.p.text" is a list that will be filled by "super.visit"
- tcv.p.text.add(tcv.p.text.size() - 2, '--BEGIN--\n')
- try {
+ return withSelectionMarkers(tcv) {
super.visitField(access, name, desc, signature, value)
- } finally {
- tcv.p.text.add('--END--\n')
}
}
}
@@ -143,6 +142,46 @@ abstract class AbstractBytecodeTestCase {
new ClassReader(bytes).accept(tcv, 0)
new InstructionSequence(instructions: out.toString().split('\n')*.trim())
}
+
+ protected List<String> compileAndFindUnreachableInstructions(Map options = [:], final String scriptText) {
+ options = [method: 'run', *: options]
+ compile(options, scriptText)
+ assert classBytes != null: 'No class bytes were captured during compilation'
+ findUnreachableInstructions(classBytes, options.method)
+ }
+
+ protected List<String> findUnreachableInstructions(final byte[] bytes, final String methodName) {
+ def classNode = new ClassNode()
+ new ClassReader(bytes).accept(classNode, ClassReader.EXPAND_FRAMES)
+
+ def methodNode = classNode.methods.find { it.name == methodName }
+ assert methodNode != null: "Method '${methodName}' was not found in ${classNode.name}"
+
+ def frames = new Analyzer(new BasicInterpreter()).analyze(classNode.name, methodNode)
+ def unreachable = []
+ for (int i = 0; i < methodNode.instructions.size(); i += 1) {
+ def instruction = methodNode.instructions.get(i)
+ if (instruction.opcode >= 0 && frames[i] == null) {
+ unreachable << "${i}:${Printer.OPCODES[instruction.opcode]}"
+ }
+ }
+ unreachable
+ }
+
+ private void captureClassBytesAndSequence(final gc, final Map options) {
+ classBytes = gc.bytes
+ sequence = extractSequence(gc.bytes, options)
+ }
+
+ private static <T> T withSelectionMarkers(final TraceClassVisitor traceClassVisitor, final Closure<T> visitAction) {
+ // The penultimate entry is the method/field body container populated by super.visit*.
+ traceClassVisitor.p.text.add(traceClassVisitor.p.text.size() - 2, '--BEGIN--\n')
+ try {
+ visitAction.call()
+ } finally {
+ traceClassVisitor.p.text.add('--END--\n')
+ }
+ }
}
/**