23
23
import com .google .javascript .jscomp .graph .DiGraph .DiGraphNode ;
24
24
import com .google .javascript .jscomp .graph .GraphReachability ;
25
25
import com .google .javascript .rhino .Node ;
26
+ import java .util .ArrayDeque ;
27
+ import java .util .Deque ;
26
28
import java .util .List ;
27
29
import java .util .logging .Level ;
28
30
import java .util .logging .Logger ;
@@ -83,29 +85,70 @@ public void enterChangedScopeRoot(AbstractCompiler compiler, Node root) {
83
85
private class EliminationPass implements NodeTraversal .Callback {
84
86
private final ControlFlowGraph <Node > cfg ;
85
87
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
+
86
96
private EliminationPass (ControlFlowGraph <Node > cfg ) {
87
97
this .cfg = cfg ;
88
98
}
89
99
90
100
@ Override
91
101
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 ()) {
95
103
// TODO(b/129564961): We should be exploring EXPORTs. We don't because their descendants
96
104
// have side-effects that `AstAnalyzer.mayHaveSideEffects` doesn't recognize. Since this
97
105
// pass currently runs after exports are removed anyway, this isn't yet an issue.
98
106
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 ));
102
130
}
103
131
104
132
return true ;
105
133
}
106
134
107
135
@ Override
108
136
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
+ }
109
152
if (parent == null || n .isFunction () || n .isScript ()) {
110
153
return ;
111
154
}
@@ -121,6 +164,32 @@ public void visit(NodeTraversal t, Node n, Node parent) {
121
164
tryRemoveUnconditionalBranching (n );
122
165
}
123
166
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
+
124
193
/**
125
194
* Tries to remove n if it is an unconditional branch node (break, continue, or return) and the
126
195
* target of n is the same as the follow of n.
@@ -179,7 +248,7 @@ private void tryRemoveUnconditionalBranching(Node n) {
179
248
Node fallThrough = computeFollowing (n );
180
249
Node nextCfgNode = outEdges .get (0 ).getDestination ().getValue ();
181
250
if (nextCfgNode == fallThrough && !inFinally (n .getParent (), n )) {
182
- removeNode (n );
251
+ logicallyRemoveNode (n );
183
252
}
184
253
}
185
254
break ;
@@ -265,10 +334,18 @@ private void removeDeadExprStatementSafely(Node n) {
265
334
return ;
266
335
}
267
336
268
- removeNode (n );
337
+ logicallyRemoveNode (n );
269
338
}
270
339
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 ) {
272
349
codeChanged = true ;
273
350
NodeUtil .redeclareVarsInsideBranch (n );
274
351
compiler .reportChangeToEnclosingScope (n );
@@ -279,4 +356,42 @@ private void removeNode(Node n) {
279
356
NodeUtil .markFunctionsDeleted (n , compiler );
280
357
}
281
358
}
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
+ }
282
397
}
0 commit comments