Skip to content

Commit e88380c

Browse files
lauraharkercopybara-github
authored andcommitted
Introduce the node SWITCH_BODY to encapsulate the cases of a switch statement.
The cases of a switch are currently children of the `SWITCH` node, along with the switch condition. This leads to some difficulties correctly modeling the new scope introduced by a switch. It holds all cases, but not the switch condition itself. Fixes a bug where the compiler incorrectly reported a variable in a switch condition was an "early reference". Introducing SWITCH_BODY makes it easier to model scope, and also in my opinion makes it easier to write code iterating over all cases in a switch - it just needs to look at all children of the SWITCH_BODY, instead of having to special case skipping the switch condition. Why not use a BLOCK? various places in the compiler assume that BLOCK implies "children of this node can be statements" - if we used a BLOCK here, we'd have to update those places to say "children can be statements unless in a SWITCH". PiperOrigin-RevId: 715051442
1 parent d7d67d1 commit e88380c

34 files changed

+227
-80
lines changed

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -1753,10 +1753,12 @@ private void validateNoCatchBinding(Node n) {
17531753
private void validateSwitch(Node n) {
17541754
validateNodeType(Token.SWITCH, n);
17551755
validateProperties(n);
1756-
validateMinimumChildCount(n, 1);
1756+
validateChildCount(n, 2);
17571757
validateExpression(n.getFirstChild());
1758+
validateNodeType(Token.SWITCH_BODY, n.getSecondChild());
1759+
Node cases = n.getSecondChild();
17581760
int defaults = 0;
1759-
for (Node c = n.getSecondChild(); c != null; c = c.getNext()) {
1761+
for (Node c = cases.getFirstChild(); c != null; c = c.getNext()) {
17601762
validateSwitchMember(c);
17611763
if (c.isDefaultCase()) {
17621764
defaults++;

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -1300,8 +1300,12 @@ protected void add(Node node, Context context, boolean printComments) {
13001300
add("switch(");
13011301
add(first);
13021302
add(")");
1303+
add(last, context);
1304+
break;
1305+
1306+
case SWITCH_BODY:
13031307
cc.beginBlock();
1304-
addAllSiblings(first.getNext());
1308+
addAllSiblings(first);
13051309
cc.endBlock(context == Context.STATEMENT);
13061310
break;
13071311

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

+17-14
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ public void visit(NodeTraversal t, Node n, Node parent) {
438438
case WITH:
439439
handleWith(n);
440440
return;
441+
case SWITCH_BODY:
441442
case LABEL:
442443
case CLASS_MEMBERS:
443444
case MEMBER_FUNCTION_DEF:
@@ -532,38 +533,40 @@ private void handleFor(Node forNode) {
532533
private void handleSwitch(Node node) {
533534
// Transfer to the first non-DEFAULT CASE. if there are none, transfer
534535
// to the DEFAULT or the EMPTY node.
535-
Node next = getNextSiblingOfType(node.getSecondChild(), Token.CASE, Token.EMPTY);
536+
Node switchBody = node.getLastChild();
537+
createEdge(node, Branch.UNCOND, switchBody);
538+
Node next = getNextSiblingOfType(switchBody.getFirstChild(), Token.CASE, Token.EMPTY);
536539
if (next != null) { // Has at least one CASE or EMPTY
537-
createEdge(node, Branch.UNCOND, next);
540+
createEdge(switchBody, Branch.UNCOND, next);
538541
} else { // Has no CASE but possibly a DEFAULT
539-
if (node.getSecondChild() != null) {
540-
createEdge(node, Branch.UNCOND, node.getSecondChild());
542+
if (switchBody.hasChildren()) {
543+
createEdge(switchBody, Branch.UNCOND, switchBody.getFirstChild());
541544
} else { // No CASE, no DEFAULT
542-
createEdge(node, Branch.UNCOND, computeFollowNode(node, this));
545+
createEdge(switchBody, Branch.UNCOND, computeFollowNode(node, this));
543546
}
544547
}
545548
connectToPossibleExceptionHandler(node, node.getFirstChild());
546549
}
547550

548-
private void handleCase(Node node) {
551+
private void handleCase(Node caseNode) {
549552
// Case is a bit tricky....First it goes into the body if condition is true.
550-
createEdge(node, Branch.ON_TRUE, node.getSecondChild());
553+
createEdge(caseNode, Branch.ON_TRUE, caseNode.getSecondChild());
551554

552555
// Look for the next CASE, skipping over DEFAULT.
553-
Node next = getNextSiblingOfType(node.getNext(), Token.CASE);
556+
Node next = getNextSiblingOfType(caseNode.getNext(), Token.CASE);
554557
if (next != null) { // Found a CASE
555558
checkState(next.isCase());
556-
createEdge(node, Branch.ON_FALSE, next);
559+
createEdge(caseNode, Branch.ON_FALSE, next);
557560
} else { // No more CASE found, go back and search for a DEFAULT.
558-
Node parent = node.getParent();
559-
Node deflt = getNextSiblingOfType(parent.getSecondChild(), Token.DEFAULT_CASE);
561+
Node switchBody = caseNode.getParent();
562+
Node deflt = getNextSiblingOfType(switchBody.getFirstChild(), Token.DEFAULT_CASE);
560563
if (deflt != null) { // Has a DEFAULT
561-
createEdge(node, Branch.ON_FALSE, deflt);
564+
createEdge(caseNode, Branch.ON_FALSE, deflt);
562565
} else { // No DEFAULT found, go to the follow of the SWITCH.
563-
createEdge(node, Branch.ON_FALSE, computeFollowNode(node, this));
566+
createEdge(caseNode, Branch.ON_FALSE, computeFollowNode(caseNode, this));
564567
}
565568
}
566-
connectToPossibleExceptionHandler(node, node.getFirstChild());
569+
connectToPossibleExceptionHandler(caseNode, caseNode.getFirstChild());
567570
}
568571

569572
private void handleDefault(Node node) {

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ public static boolean isEnteringNewCfgNode(Node n) {
146146
case ROOT:
147147
case SCRIPT:
148148
case TRY:
149+
case SWITCH_BODY:
149150
return true;
150151
case FUNCTION:
151152
// A function node represents the start of a function where the name
@@ -176,11 +177,10 @@ public static boolean isEnteringNewCfgNode(Node n) {
176177
// TODO(user): Investigate how we should handle the case where
177178
// we have a very complex expression inside the FOR-IN header.
178179
return n != parent.getFirstChild();
179-
case SWITCH:
180180
case CASE:
181181
case CATCH:
182182
case WITH:
183-
return n != parent.getFirstChild();
183+
return n != parent.getFirstChild(); // exclude the condition
184184
default:
185185
return false;
186186
}

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

+6-3
Original file line numberDiff line numberDiff line change
@@ -969,7 +969,8 @@ void transpileSwitch(Node n, TranspilationContext.@Nullable Case breakCase) {
969969

970970
// Are all "switch" cases unmarked?
971971
boolean hasGeneratorMarker = false;
972-
for (Node caseSection = n.getSecondChild();
972+
Node switchBody = n.getSecondChild();
973+
for (Node caseSection = switchBody.getFirstChild();
973974
caseSection != null;
974975
caseSection = caseSection.getNext()) {
975976
if (caseSection.isGeneratorMarker()) {
@@ -1000,7 +1001,7 @@ class SwitchCase {
10001001

10011002
// We don't have to transpile unmarked cases at the beginning of "switch".
10021003
boolean canSkipUnmarkedCases = true;
1003-
for (Node caseSection = n.getSecondChild();
1004+
for (Node caseSection = switchBody.getFirstChild();
10041005
caseSection != null;
10051006
caseSection = caseSection.getNext()) {
10061007
if (!caseSection.isDefaultCase() && caseSection.getFirstChild().isGeneratorMarker()) {
@@ -1046,6 +1047,7 @@ class SwitchCase {
10461047

10471048
// Transpile the barebone of original "switch" statement
10481049
n.setGeneratorMarker(false);
1050+
switchBody.setGeneratorMarker(false);
10491051
transpileUnmarkedNode(n);
10501052
context.writeJumpTo(endCase, n); // TODO(skill): do not always add this.
10511053

@@ -1359,10 +1361,11 @@ public void finalizeTransformation(Node generatorBody) {
13591361
Node switchNode =
13601362
IR.switchNode(getContextField(generatorBody, "nextAddress")).srcref(generatorBody);
13611363
generatorBody.addChildToBack(switchNode);
1364+
Node switchBody = switchNode.getSecondChild().srcref(generatorBody);
13621365

13631366
// Populate "switch" statement with "case"s.
13641367
for (Case currentCase : allCases) {
1365-
switchNode.addChildToBack(currentCase.createCaseNode());
1368+
switchBody.addChildToBack(currentCase.createCaseNode());
13661369
}
13671370
allCases.clear();
13681371
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,7 @@ private static boolean isConditionalOp(Node n) {
924924
case BLOCK:
925925
case LABEL:
926926
case CASE:
927+
case SWITCH_BODY:
927928
case DEFAULT_CASE:
928929
case DEFAULT_VALUE:
929930
case PARAM_LIST:

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -411,9 +411,11 @@ private void splitObject(Var v, Reference init, ReferenceCollection referenceInf
411411
// Assuming scopeRoot is a BLOCK, then we want to insert at the top of the block, before the
412412
// first statement.
413413
vnode = scopeRoot.getFirstChild();
414-
// Some scope-creating nodes might not be BLOCK nodes (e.g. SWITCH)
414+
// Some scope-creating nodes might not be BLOCK nodes
415415
if (!NodeUtil.isStatement(vnode) && NodeUtil.isStatement(scopeRoot)) {
416416
vnode = scopeRoot;
417+
} else if (scopeRoot.isSwitchBody()) {
418+
vnode = scopeRoot.getParent();
417419
}
418420
} else {
419421
// Find the beginning of the function body / script.

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,9 @@ void tryMinimizeExits(Node n, Token exitType, @Nullable String labelName) {
200200
void tryMinimizeSwitchExits(Node n, Token exitType, @Nullable String labelName) {
201201
checkState(n.isSwitch());
202202
// Skipping the switch condition, visit all the children.
203-
for (Node c = n.getSecondChild(); c != null; c = c.getNext()) {
204-
if (c != n.getLastChild()) {
203+
Node switchBody = n.getSecondChild();
204+
for (Node c = switchBody.getFirstChild(); c != null; c = c.getNext()) {
205+
if (c != switchBody.getLastChild()) {
205206
tryMinimizeSwitchCaseExits(c, exitType, labelName);
206207
} else {
207208
// Last case, the last case block can be optimized more aggressively.

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@ private void traverseAtScope(AbstractScope<?, ?> s) {
656656
}
657657
break;
658658
case BLOCK:
659-
case SWITCH:
659+
case SWITCH_BODY:
660660
if (callback.shouldTraverse(this, n, null)) {
661661
pushScope(s);
662662

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

+6-3
Original file line numberDiff line numberDiff line change
@@ -2450,7 +2450,7 @@ static boolean isControlStructureCodeBlock(Node parent, Node n) {
24502450
case IF:
24512451
case SWITCH:
24522452
case CASE:
2453-
return parent.getFirstChild() != n;
2453+
return parent.getFirstChild() != n; // exclude the condition
24542454
case DEFAULT_CASE:
24552455
return true;
24562456
default:
@@ -2515,7 +2515,7 @@ static boolean createsBlockScope(Node n) {
25152515
case FOR_IN:
25162516
case FOR_OF:
25172517
case FOR_AWAIT_OF:
2518-
case SWITCH:
2518+
case SWITCH_BODY:
25192519
case CLASS:
25202520
return true;
25212521
default:
@@ -4533,7 +4533,10 @@ public boolean apply(Node n) {
45334533
* traversing into nested functions, so it will return true for e.g. a BLOCK or SWITCH statement.
45344534
*/
45354535
public static boolean isShallowStatementTree(Node n) {
4536-
return n == null || NodeUtil.isControlStructure(n) || NodeUtil.isStatementBlock(n);
4536+
return n == null
4537+
|| NodeUtil.isControlStructure(n)
4538+
|| NodeUtil.isStatementBlock(n)
4539+
|| n.isSwitchBody();
45374540
}
45384541

45394542
/** A predicate for matching statements without exiting the current scope. */

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

+24-23
Original file line numberDiff line numberDiff line change
@@ -511,31 +511,32 @@ private void removeIfUnnamedBreak(Node maybeBreak) {
511511
}
512512
}
513513

514-
private Node tryRemoveSwitchWithSingleCase(Node n, boolean shouldHoistCondition) {
515-
Node caseBlock = n.getLastChild().getLastChild();
514+
private Node tryRemoveSwitchWithSingleCase(Node switchNode, boolean shouldHoistCondition) {
515+
Node switchBody = switchNode.getSecondChild();
516+
Node caseBlock = switchBody.getOnlyChild().getLastChild();
516517
removeIfUnnamedBreak(caseBlock.getLastChild());
517518
// Back off if the switch contains statements like "if (a) { break; }"
518519
if (NodeUtil.has(caseBlock, MATCH_UNNAMED_BREAK, NodeUtil.MATCH_NOT_FUNCTION)) {
519-
return n;
520+
return switchNode;
520521
}
521522
if (shouldHoistCondition) {
522-
Node switchBlock = caseBlock.getGrandparent();
523-
IR.exprResult(n.removeFirstChild()).srcref(n).insertBefore(switchBlock);
523+
IR.exprResult(switchNode.removeFirstChild()).srcref(switchNode).insertBefore(switchNode);
524524
}
525-
n.replaceWith(caseBlock.detach());
525+
switchNode.replaceWith(caseBlock.detach());
526526
reportChangeToEnclosingScope(caseBlock);
527527
return caseBlock;
528528
}
529529

530530
private Node tryRemoveSwitch(Node n) {
531-
if (n.hasOneChild()) {
531+
Node switchBody = n.getSecondChild();
532+
if (!switchBody.hasChildren()) {
532533
// Remove the switch if there are no remaining cases
533534
Node condition = n.removeFirstChild();
534535
Node replacement = IR.exprResult(condition).srcref(n);
535536
n.replaceWith(replacement);
536537
reportChangeToEnclosingScope(replacement);
537538
return replacement;
538-
} else if (n.hasTwoChildren() && n.getLastChild().isDefaultCase()) {
539+
} else if (switchBody.hasOneChild() && switchBody.getOnlyChild().isDefaultCase()) {
539540
if (n.getFirstChild().isCall() || n.getFirstChild().isOptChainCall()) {
540541
// Before removing switch, we must preserve the switch condition if it is a call
541542
return tryRemoveSwitchWithSingleCase(n, true);
@@ -551,16 +552,17 @@ private Node tryRemoveSwitch(Node n) {
551552
private Node tryOptimizeSwitch(Node n) {
552553
checkState(n.isSwitch(), n);
553554

555+
Node switchBody = n.getSecondChild();
554556
Node defaultCase = tryOptimizeDefaultCase(n);
555557

556558
// Generally, it is unsafe to remove other cases when the default case is not the last one.
557-
if (defaultCase == null || n.getLastChild().isDefaultCase()) {
559+
if (defaultCase == null || switchBody.getLastChild().isDefaultCase()) {
558560
Node cond = n.getFirstChild();
559561
Node prev = null;
560562
Node next = null;
561563
Node cur;
562564

563-
for (cur = cond.getNext(); cur != null; cur = next) {
565+
for (cur = switchBody.getFirstChild(); cur != null; cur = next) {
564566
next = cur.getNext();
565567
if (!mayHaveSideEffects(cur.getFirstChild()) && isUselessCase(cur, prev, defaultCase)) {
566568
removeCase(n, cur);
@@ -574,7 +576,7 @@ private Node tryOptimizeSwitch(Node n) {
574576
Node caseLabel;
575577
Tri caseMatches = Tri.TRUE;
576578
// Remove cases until you find one that may match
577-
for (cur = cond.getNext(); cur != null; cur = next) {
579+
for (cur = switchBody.getFirstChild(); cur != null; cur = next) {
578580
next = cur.getNext();
579581
caseLabel = cur.getFirstChild();
580582
caseMatches = PeepholeFoldConstants.evaluateComparison(this, Token.SHEQ, cond, caseLabel);
@@ -637,23 +639,21 @@ private Node tryOptimizeSwitch(Node n) {
637639
private @Nullable Node tryOptimizeDefaultCase(Node n) {
638640
checkState(n.isSwitch(), n);
639641

640-
Node lastNonRemovable = n.getFirstChild(); // The switch condition
642+
Node switchBody = n.getSecondChild();
643+
Node lastNonRemovable = null; // the most recently iterated case known to not be removable.
641644

642-
// The first child is the switch conditions skip it when looking for cases.
643-
for (Node c = n.getSecondChild(); c != null; c = c.getNext()) {
645+
for (Node c = switchBody.getFirstChild(); c != null; c = c.getNext()) {
644646
if (c.isDefaultCase()) {
645-
// Remove cases that fall-through to the default case
646-
Node caseToRemove = lastNonRemovable.getNext();
647+
// Remove any cases that fall-through to the default case
648+
Node caseToRemove =
649+
lastNonRemovable != null ? lastNonRemovable.getNext() : switchBody.getFirstChild();
647650
for (Node next; caseToRemove != c; caseToRemove = next) {
648651
next = caseToRemove.getNext();
649652
removeCase(n, caseToRemove);
650653
}
651654

652-
// Don't use the switch condition as the previous case.
653-
Node prevCase = (lastNonRemovable == n.getFirstChild()) ? null : lastNonRemovable;
654-
655655
// Remove the default case if we can
656-
if (isUselessCase(c, prevCase, c)) {
656+
if (isUselessCase(c, lastNonRemovable, c)) {
657657
removeCase(n, c);
658658
return null;
659659
}
@@ -690,8 +690,8 @@ private boolean isUselessCase(
690690
checkState(previousCase == null || previousCase.getNext() == caseNode);
691691
// A case isn't useless if a previous case falls through to it unless it happens to be the last
692692
// case in the switch.
693-
Node switchNode = caseNode.getParent();
694-
if (switchNode.getLastChild() != caseNode && previousCase != null) {
693+
Node switchBody = caseNode.getParent();
694+
if (switchBody.getLastChild() != caseNode && previousCase != null) {
695695
Node previousBlock = previousCase.getLastChild();
696696
if (!previousBlock.hasChildren() || !isExit(previousBlock.getLastChild())) {
697697
return false;
@@ -818,7 +818,8 @@ private static boolean isSwitchExit(Node n) {
818818

819819
boolean hasDefaultCase = false;
820820

821-
for (Node switchCase = n.getSecondChild();
821+
Node switchBody = n.getSecondChild();
822+
for (Node switchCase = switchBody.getFirstChild();
822823
switchCase != null;
823824
switchCase = switchCase.getNext()) {
824825
if (switchCase.isDefaultCase()) {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ private static boolean isDefinitelyRValue(Node rvalue) {
309309
case IF:
310310
case SWITCH:
311311
case WHILE:
312-
return rvalue.isFirstChildOf(parent);
312+
return rvalue.isFirstChildOf(parent); // the condition is always an r-value
313313

314314
case EXPR_RESULT:
315315
// Extern declarations are sometimes stubs. These must be considered L-values with no

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ private static boolean isBlockBoundary(Node n, Node parent) {
293293
case WHILE:
294294
case WITH:
295295
case CLASS:
296+
case SWITCH_BODY:
296297
// NOTE: TRY has up to 3 child blocks:
297298
// TRY
298299
// BLOCK
@@ -307,7 +308,6 @@ private static boolean isBlockBoundary(Node n, Node parent) {
307308
case HOOK:
308309
case IF:
309310
case OR:
310-
case SWITCH:
311311
case COALESCE:
312312
case OPTCHAIN_GETPROP:
313313
case OPTCHAIN_GETELEM:

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ private void traverseNode(Node n, Scope scope) {
463463
traverseCall(n, scope);
464464
break;
465465

466-
case SWITCH:
466+
case SWITCH_BODY:
467467
case BLOCK:
468468
// This case if for if there are let and const variables in block scopes.
469469
// Otherwise other variables will be hoisted up into the global scope and already be

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ void populate() {
151151
case FOR_OF:
152152
case FOR_AWAIT_OF:
153153
case FOR_IN:
154-
case SWITCH:
154+
case SWITCH_BODY:
155155
scanVars(n, null, scope);
156156
return;
157157

0 commit comments

Comments
 (0)