Skip to content

Commit 0e9335f

Browse files
brad4dcopybara-github
authored andcommitted
UnreachableCodeElimination: Avoid traversing unreachable statements
When traversing a block of statements, skip traversal of all statements following the first one we find to be unreachable and remove them all. This avoids unnecessary work traversing the individual statements that we already know will not ever execute. This change also fixes a bug that caused unreachable `do {} while (condition)` statements to be left behind. This could cause an undeclared variable reference if the condition referred to a `let` or `const` variable whose declaration was also removed as unreachable. That bug was the original motivation for this change. PiperOrigin-RevId: 565691433
1 parent 5080646 commit 0e9335f

File tree

2 files changed

+151
-16
lines changed

2 files changed

+151
-16
lines changed

src/com/google/javascript/jscomp/UnreachableCodeElimination.java

Lines changed: 124 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import com.google.javascript.jscomp.graph.DiGraph.DiGraphNode;
2424
import com.google.javascript.jscomp.graph.GraphReachability;
2525
import com.google.javascript.rhino.Node;
26+
import java.util.ArrayDeque;
27+
import java.util.Deque;
2628
import java.util.List;
2729
import java.util.logging.Level;
2830
import java.util.logging.Logger;
@@ -83,29 +85,70 @@ public void enterChangedScopeRoot(AbstractCompiler compiler, Node root) {
8385
private class EliminationPass implements NodeTraversal.Callback {
8486
private final ControlFlowGraph<Node> cfg;
8587

88+
/**
89+
* Keep track of nodes that contain a sequence of statements.
90+
*
91+
* <p>As soon as we find one statement is unreachable, we can skip traversing the rest.
92+
*/
93+
private final Deque<StatementSequenceParentContext> statementSequenceParentContextStack =
94+
new ArrayDeque<>();
95+
8696
private EliminationPass(ControlFlowGraph<Node> cfg) {
8797
this.cfg = cfg;
8898
}
8999

90100
@Override
91101
public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
92-
if (parent == null) {
93-
return true;
94-
} else if (n.isExport()) {
102+
if (n.isExport()) {
95103
// TODO(b/129564961): We should be exploring EXPORTs. We don't because their descendants
96104
// have side-effects that `AstAnalyzer.mayHaveSideEffects` doesn't recognize. Since this
97105
// pass currently runs after exports are removed anyway, this isn't yet an issue.
98106
return false;
99-
} else if (parent.isFunction()) {
100-
// We only want to traverse the name of a function.
101-
return n.isFirstChildOf(parent);
107+
} else if (n.isFunction()) {
108+
// Do not descend into function scopes, because they won't be included in our
109+
// current CFG.
110+
return false;
111+
}
112+
113+
StatementSequenceParentContext statementSequenceParentContext =
114+
statementSequenceParentContextStack.peek();
115+
if (statementSequenceParentContext != null
116+
&& statementSequenceParentContext.statementParentNode == parent) {
117+
// We're looking at a statement node in the current statement parent
118+
if (statementSequenceParentContext.firstUnreachableStatementNode != null) {
119+
// A previous statement is unreachable, so there's no point looking at this one.
120+
return false;
121+
}
122+
if (isDefinitelyUnreachable(n)) {
123+
statementSequenceParentContext.firstUnreachableStatementNode = n;
124+
return false;
125+
}
126+
}
127+
128+
if (isStatementSequenceParent(n)) {
129+
statementSequenceParentContextStack.push(new StatementSequenceParentContext(n));
102130
}
103131

104132
return true;
105133
}
106134

107135
@Override
108136
public void visit(NodeTraversal t, Node n, Node parent) {
137+
StatementSequenceParentContext statementSequenceParentContext =
138+
statementSequenceParentContextStack.peek();
139+
if (statementSequenceParentContext != null
140+
&& statementSequenceParentContext.statementParentNode == n) {
141+
// We're now visiting the statement parent, itself.
142+
statementSequenceParentContextStack.pop();
143+
Node unreachableStatementNode =
144+
statementSequenceParentContext.firstUnreachableStatementNode;
145+
while (unreachableStatementNode != null) {
146+
final Node nextStatement = unreachableStatementNode.getNext();
147+
removeStatementNode(unreachableStatementNode);
148+
unreachableStatementNode = nextStatement;
149+
}
150+
return;
151+
}
109152
if (parent == null || n.isFunction() || n.isScript()) {
110153
return;
111154
}
@@ -121,6 +164,32 @@ public void visit(NodeTraversal t, Node n, Node parent) {
121164
tryRemoveUnconditionalBranching(n);
122165
}
123166

167+
private boolean isDefinitelyUnreachable(Node n) {
168+
DiGraphNode<Node, Branch> gNode = getCfgNodeForStatement(n);
169+
if (gNode == null) {
170+
// Not in CFG.
171+
// We may have traversed into a scope not covered by the CFG,
172+
// or maybe just looking at a node the CFG doesn't consider part of the control flow.
173+
return false;
174+
}
175+
return gNode.getAnnotation() != GraphReachability.REACHABLE;
176+
}
177+
178+
private DiGraphNode<Node, Branch> getCfgNodeForStatement(Node statement) {
179+
switch (statement.getToken()) {
180+
case DO:
181+
// CFG flows first into the statement within the do {} while ();
182+
// So we should consider that CFG node to represent the whole statement.
183+
return cfg.getNode(statement.getFirstChild());
184+
case LABEL:
185+
// A LABEL is never actually executed, so get what it labels.
186+
// We use recursion because it is possible to label a label.
187+
return getCfgNodeForStatement(statement.getLastChild());
188+
default:
189+
return cfg.getNode(statement);
190+
}
191+
}
192+
124193
/**
125194
* Tries to remove n if it is an unconditional branch node (break, continue, or return) and the
126195
* target of n is the same as the follow of n.
@@ -179,7 +248,7 @@ private void tryRemoveUnconditionalBranching(Node n) {
179248
Node fallThrough = computeFollowing(n);
180249
Node nextCfgNode = outEdges.get(0).getDestination().getValue();
181250
if (nextCfgNode == fallThrough && !inFinally(n.getParent(), n)) {
182-
removeNode(n);
251+
logicallyRemoveNode(n);
183252
}
184253
}
185254
break;
@@ -265,10 +334,18 @@ private void removeDeadExprStatementSafely(Node n) {
265334
return;
266335
}
267336

268-
removeNode(n);
337+
logicallyRemoveNode(n);
269338
}
270339

271-
private void removeNode(Node n) {
340+
/**
341+
* Logically, put possibly not actually, remove a node.
342+
*
343+
* <p>This method uses {@code NodeUtil.removeChild()} which has a lot of logic to handle
344+
* attempts to remove nodes that are structurally required by the AST. It will make a change
345+
* that has the behavior of the node being removed, even though what actually is done to the AST
346+
* may not be simple removal of the node.
347+
*/
348+
private void logicallyRemoveNode(Node n) {
272349
codeChanged = true;
273350
NodeUtil.redeclareVarsInsideBranch(n);
274351
compiler.reportChangeToEnclosingScope(n);
@@ -279,4 +356,42 @@ private void removeNode(Node n) {
279356
NodeUtil.markFunctionsDeleted(n, compiler);
280357
}
281358
}
359+
360+
/**
361+
* Remove a statement that is part of a sequence of statements.
362+
*
363+
* <p>Unlike {@code logicallyRemoveNode()}, this method will always remove the node.
364+
*/
365+
private void removeStatementNode(Node statementNode) {
366+
codeChanged = true;
367+
NodeUtil.redeclareVarsInsideBranch(statementNode);
368+
compiler.reportChangeToEnclosingScope(statementNode);
369+
if (logger.isLoggable(Level.FINE)) {
370+
logger.fine("Removing " + statementNode);
371+
}
372+
// Since we know we have a statement within a statement sequence here, simply detaching it is
373+
// always safe.
374+
statementNode.detach();
375+
NodeUtil.markFunctionsDeleted(statementNode, compiler);
376+
}
377+
378+
/** Is {@code n} a {@code Node} that has a sequence of statements as its children? */
379+
private static boolean isStatementSequenceParent(Node n) {
380+
// A LABEL is a statement parent, but only for a single statement.
381+
// For historical reasons, the second child of a TRY is a BLOCK with a single CATCH child.
382+
// We don't want to treat the CATCH as if it were a statement.
383+
return NodeUtil.isStatementParent(n) && !n.isLabel() && !NodeUtil.isTryCatchNodeContainer(n);
384+
}
385+
386+
/** One of these is created for each node whose children are a sequence of statements. */
387+
private static class StatementSequenceParentContext {
388+
final Node statementParentNode;
389+
390+
/** Set non-null only if we discover that some statements are unreachable. */
391+
Node firstUnreachableStatementNode = null;
392+
393+
public StatementSequenceParentContext(Node statementParentNode) {
394+
this.statementParentNode = statementParentNode;
395+
}
396+
}
282397
}

test/com/google/javascript/jscomp/UnreachableCodeEliminationTest.java

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,26 @@ public void setUp() throws Exception {
3939
enableComputeSideEffects();
4040
}
4141

42+
@Test
43+
public void testLabeledBlocks() {
44+
test(
45+
lines(
46+
"function b(m) {", //
47+
" return m;",
48+
" label: {",
49+
" START('debug');",
50+
" label2: {",
51+
" alert('Shouldnt be here' + m);",
52+
" }",
53+
" END('debug');",
54+
" }",
55+
"}"),
56+
lines(
57+
"function b(m) {", //
58+
" return m;",
59+
"}"));
60+
}
61+
4262
@Test
4363
public void testDoNotRemoveDeclarationOfUsedVariable() {
4464
test(
@@ -54,10 +74,6 @@ public void testDoNotRemoveDeclarationOfUsedVariable() {
5474
lines(
5575
"var f = function() {", //
5676
" return 1;",
57-
" do {",
58-
// TODO: b/297246830 - This is broken behavior.
59-
// Either delete all references to `b` or keep its declaration
60-
" } while (b);",
6177
"};"));
6278
}
6379

@@ -403,7 +419,7 @@ public void testRemoveDo() {
403419
// for-loops.
404420
test(
405421
"for (; 1;) { break; do { print(1); break } while(1) }",
406-
"for (; 1;) { break; do {} while(1) }");
422+
"for (; 1;) { break; }");
407423
}
408424

409425
@Test
@@ -747,9 +763,13 @@ public void testIssue4177428_multifinally() {
747763

748764
@Test
749765
public void testIssue5215541_deadVarDeclar() {
750-
testSame("throw 1; var x");
766+
test(
767+
" throw 1; var x;", //
768+
"var x; throw 1; ");
751769
testSame("throw 1; function x() {}");
752-
testSame("throw 1; var x; var y;");
770+
test(
771+
"throw 1; var x; var y; ", //
772+
" var y; var x; throw 1;");
753773
test(
754774
" throw 1; var x = foo", //
755775
"var x; throw 1");

0 commit comments

Comments
 (0)