Skip to content

Commit e418411

Browse files
markhbradyError Prone Team
authored and
Error Prone Team
committed
StatementSwitchToExpressionSwitch: reachability analysis
PiperOrigin-RevId: 735958054
1 parent ee8702c commit e418411

File tree

3 files changed

+329
-44
lines changed

3 files changed

+329
-44
lines changed

check_api/src/main/java/com/google/errorprone/util/Reachability.java

+88-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static com.google.errorprone.util.ASTHelpers.isSwitchDefault;
2323
import static java.util.Objects.requireNonNull;
2424

25+
import com.google.common.collect.ImmutableMap;
2526
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2627
import com.sun.source.tree.AssertTree;
2728
import com.sun.source.tree.BlockTree;
@@ -66,7 +67,20 @@ public class Reachability {
6667
* <p>An exception is made for {@code System.exit}, which cannot complete normally in practice.
6768
*/
6869
public static boolean canCompleteNormally(StatementTree statement) {
69-
return statement.accept(new CanCompleteNormallyVisitor(), null);
70+
return statement.accept(new CanCompleteNormallyVisitor(ImmutableMap.of()), null);
71+
}
72+
73+
/**
74+
* Returns true if the given statement can complete normally, as defined by JLS 14.21, when taking
75+
* into account the given patches. The patches are a (possibly empty) map from {@code Tree} to a
76+
* boolean indicating whether that specific {@code Tree} can complete normally. For all trees not
77+
* present in the patches, they will be analyzed as normal.
78+
*
79+
* <p>An exception is made for {@code System.exit}, which cannot complete normally in practice.
80+
*/
81+
public static boolean canCompleteNormally(
82+
StatementTree statement, ImmutableMap<Tree, Boolean> patches) {
83+
return statement.accept(new CanCompleteNormallyVisitor(patches), null);
7084
}
7185

7286
/**
@@ -100,10 +114,21 @@ private static class CanCompleteNormallyVisitor extends SimpleTreeVisitor<Boolea
100114
/** Trees that are the target of a reachable continue statement. */
101115
private final Set<Tree> continues = new HashSet<>();
102116

117+
/** Trees that are patched to have a specific completion result. */
118+
private final ImmutableMap<Tree, Boolean> patches;
119+
120+
public CanCompleteNormallyVisitor(ImmutableMap<Tree, Boolean> patches) {
121+
this.patches = patches;
122+
}
123+
103124
boolean scan(List<? extends StatementTree> trees) {
104125
boolean completes = true;
105126
for (StatementTree tree : trees) {
106-
completes = scan(tree);
127+
if (patches.containsKey(tree)) {
128+
completes = patches.get(tree);
129+
} else {
130+
completes = scan(tree);
131+
}
107132
}
108133
return completes;
109134
}
@@ -112,12 +137,18 @@ boolean scan(List<? extends StatementTree> trees) {
112137
// don't otherwise affect the result of the reachability analysis.
113138
@CanIgnoreReturnValue
114139
private boolean scan(Tree tree) {
140+
if (patches.containsKey(tree)) {
141+
return patches.get(tree);
142+
}
115143
return tree.accept(this, null);
116144
}
117145

118146
/* A break statement cannot complete normally. */
119147
@Override
120148
public Boolean visitBreak(BreakTree tree, Void unused) {
149+
if (patches.containsKey(tree)) {
150+
return patches.get(tree);
151+
}
121152
breaks.add(skipLabel(requireNonNull(((JCTree.JCBreak) tree).target)));
122153
return false;
123154
}
@@ -126,6 +157,9 @@ public Boolean visitBreak(BreakTree tree, Void unused) {
126157
@Override
127158
public Boolean visitContinue(ContinueTree tree, Void unused) {
128159
continues.add(skipLabel(requireNonNull(((JCTree.JCContinue) tree).target)));
160+
if (patches.containsKey(tree)) {
161+
return patches.get(tree);
162+
}
129163
return false;
130164
}
131165

@@ -135,29 +169,44 @@ private static Tree skipLabel(JCTree tree) {
135169

136170
@Override
137171
public Boolean visitBlock(BlockTree tree, Void unused) {
172+
if (patches.containsKey(tree)) {
173+
return patches.get(tree);
174+
}
138175
return scan(tree.getStatements());
139176
}
140177

141178
/* A local class declaration statement can complete normally iff it is reachable. */
142179
@Override
143180
public Boolean visitClass(ClassTree tree, Void unused) {
181+
if (patches.containsKey(tree)) {
182+
return patches.get(tree);
183+
}
144184
return true;
145185
}
146186

147187
/* A local variable declaration statement can complete normally iff it is reachable. */
148188
@Override
149189
public Boolean visitVariable(VariableTree tree, Void unused) {
190+
if (patches.containsKey(tree)) {
191+
return patches.get(tree);
192+
}
150193
return true;
151194
}
152195

153196
/* An empty statement can complete normally iff it is reachable. */
154197
@Override
155198
public Boolean visitEmptyStatement(EmptyStatementTree tree, Void unused) {
199+
if (patches.containsKey(tree)) {
200+
return patches.get(tree);
201+
}
156202
return true;
157203
}
158204

159205
@Override
160206
public Boolean visitLabeledStatement(LabeledStatementTree tree, Void unused) {
207+
if (patches.containsKey(tree)) {
208+
return patches.get(tree);
209+
}
161210
// break/continue targets have already been resolved by javac, so
162211
// there's nothing to do here
163212
return scan(tree.getStatement());
@@ -166,6 +215,9 @@ public Boolean visitLabeledStatement(LabeledStatementTree tree, Void unused) {
166215
/* An expression statement can complete normally iff it is reachable. */
167216
@Override
168217
public Boolean visitExpressionStatement(ExpressionStatementTree tree, Void unused) {
218+
if (patches.containsKey(tree)) {
219+
return patches.get(tree);
220+
}
169221
if (isSystemExit(tree.getExpression())) {
170222
// The spec doesn't have any special handling for {@code System.exit}, but in practice it
171223
// cannot complete normally
@@ -198,6 +250,9 @@ private static boolean isSystemExit(ExpressionTree expression) {
198250
*/
199251
@Override
200252
public Boolean visitIf(IfTree tree, Void unused) {
253+
if (patches.containsKey(tree)) {
254+
return patches.get(tree);
255+
}
201256
boolean thenCompletes = scan(tree.getThenStatement());
202257
boolean elseCompletes = tree.getElseStatement() == null || scan(tree.getElseStatement());
203258
return thenCompletes || elseCompletes;
@@ -206,6 +261,9 @@ public Boolean visitIf(IfTree tree, Void unused) {
206261
/* An assert statement can complete normally iff it is reachable. */
207262
@Override
208263
public Boolean visitAssert(AssertTree tree, Void unused) {
264+
if (patches.containsKey(tree)) {
265+
return patches.get(tree);
266+
}
209267
return true;
210268
}
211269

@@ -232,6 +290,10 @@ public Boolean visitAssert(AssertTree tree, Void unused) {
232290
*/
233291
@Override
234292
public Boolean visitSwitch(SwitchTree tree, Void unused) {
293+
if (patches.containsKey(tree)) {
294+
return patches.get(tree);
295+
}
296+
235297
// (1)
236298
if (tree.getCases().stream().noneMatch(c -> isSwitchDefault(c))) {
237299
return true;
@@ -294,6 +356,9 @@ public Boolean visitSwitch(SwitchTree tree, Void unused) {
294356
*/
295357
@Override
296358
public Boolean visitWhileLoop(WhileLoopTree tree, Void unused) {
359+
if (patches.containsKey(tree)) {
360+
return patches.get(tree);
361+
}
297362
Boolean condValue = ASTHelpers.constValue(tree.getCondition(), Boolean.class);
298363
if (!Objects.equals(condValue, false)) {
299364
scan(tree.getStatement());
@@ -333,6 +398,9 @@ public Boolean visitWhileLoop(WhileLoopTree tree, Void unused) {
333398
*/
334399
@Override
335400
public Boolean visitDoWhileLoop(DoWhileLoopTree that, Void unused) {
401+
if (patches.containsKey(that)) {
402+
return patches.get(that);
403+
}
336404
boolean completes = scan(that.getStatement());
337405
boolean conditionIsAlwaysTrue =
338406
firstNonNull(ASTHelpers.constValue(that.getCondition(), Boolean.class), false);
@@ -367,6 +435,9 @@ public Boolean visitDoWhileLoop(DoWhileLoopTree that, Void unused) {
367435
*/
368436
@Override
369437
public Boolean visitForLoop(ForLoopTree that, Void unused) {
438+
if (patches.containsKey(that)) {
439+
return patches.get(that);
440+
}
370441
Boolean condValue = ASTHelpers.constValue(that.getCondition(), Boolean.class);
371442
if (!Objects.equals(condValue, false)) {
372443
scan(that.getStatement());
@@ -385,19 +456,28 @@ public Boolean visitForLoop(ForLoopTree that, Void unused) {
385456
/* An enhanced for statement can complete normally iff it is reachable. */
386457
@Override
387458
public Boolean visitEnhancedForLoop(EnhancedForLoopTree that, Void unused) {
459+
if (patches.containsKey(that)) {
460+
return patches.get(that);
461+
}
388462
scan(that.getStatement());
389463
return true;
390464
}
391465

392466
/* A return statement cannot complete normally. */
393467
@Override
394468
public Boolean visitReturn(ReturnTree tree, Void unused) {
469+
if (patches.containsKey(tree)) {
470+
return patches.get(tree);
471+
}
395472
return false;
396473
}
397474

398475
/* A throw statement cannot complete normally. */
399476
@Override
400477
public Boolean visitThrow(ThrowTree tree, Void unused) {
478+
if (patches.containsKey(tree)) {
479+
return patches.get(tree);
480+
}
401481
return false;
402482
}
403483

@@ -410,6 +490,9 @@ public Boolean visitThrow(ThrowTree tree, Void unused) {
410490
*/
411491
@Override
412492
public Boolean visitSynchronized(SynchronizedTree tree, Void unused) {
493+
if (patches.containsKey(tree)) {
494+
return patches.get(tree);
495+
}
413496
return scan(tree.getBlock());
414497
}
415498

@@ -424,6 +507,9 @@ public Boolean visitSynchronized(SynchronizedTree tree, Void unused) {
424507
*/
425508
@Override
426509
public Boolean visitTry(TryTree that, Void unused) {
510+
if (patches.containsKey(that)) {
511+
return patches.get(that);
512+
}
427513
boolean completes = scan(that.getBlock());
428514
// assume all catch blocks are reachable; javac has already rejected unreachable
429515
// checked exception handlers

core/src/main/java/com/google/errorprone/bugpatterns/StatementSwitchToExpressionSwitch.java

+52-41
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.google.common.collect.HashBiMap;
4040
import com.google.common.collect.ImmutableBiMap;
4141
import com.google.common.collect.ImmutableList;
42+
import com.google.common.collect.ImmutableMap;
4243
import com.google.common.collect.ImmutableSet;
4344
import com.google.common.collect.Iterables;
4445
import com.google.common.collect.Streams;
@@ -1082,17 +1083,47 @@ private static SuggestedFix convertToReturnSwitch(
10821083
// Close the switch statement
10831084
replacementCodeBuilder.append("\n};");
10841085

1085-
// Statements in the same block following the switch are currently reachable but will become
1086-
// unreachable, which would lead to a compile-time error. Therefore, suggest that they be
1087-
// removed.
1088-
statementsToDelete.addAll(followingStatementsInBlock(switchTree, state));
1086+
// The transformed code can cause other existing code to become dead code. So, we must analyze
1087+
// and delete such dead code, otherwise the suggested autofix could fail to compile.
1088+
1089+
// The `return switch ...` will always return or throw
1090+
Tree cannotCompleteNormallyTree = switchTree;
1091+
// Search up the AST for enclosing statement blocks, marking any newly-dead code for deletion
1092+
// along the way
1093+
for (TreePath enclosingPath = state.getPath();
1094+
enclosingPath != null;
1095+
enclosingPath = enclosingPath.getParentPath()) {
1096+
1097+
if (enclosingPath.getLeaf() instanceof BlockTree blockTree) {
1098+
TreePath rootToCurrentPath = TreePath.getPath(state.getPath(), switchTree);
1099+
int indexInBlock = findBlockStatementIndex(rootToCurrentPath, blockTree);
1100+
// A single mock of the immediate child statement block (or switch) is sufficient to
1101+
// analyze reachability here; deeper-nested statements are not relevant.
1102+
boolean nextStatementReachable =
1103+
Reachability.canCompleteNormally(
1104+
blockTree.getStatements().get(indexInBlock),
1105+
ImmutableMap.of(cannotCompleteNormallyTree, false));
1106+
// If we continue to the ancestor statement block, it will be because the end of this
1107+
// statement block is not reachable
1108+
cannotCompleteNormallyTree = blockTree;
1109+
if (nextStatementReachable) {
1110+
break;
1111+
}
1112+
1113+
// If the next statement is not reachable, then none of the following statements in this
1114+
// block are either. So, we need to delete them all.
1115+
for (int i = indexInBlock + 1; (i >= 0) && (i < blockTree.getStatements().size()); i++) {
1116+
statementsToDelete.add(blockTree.getStatements().get(i));
1117+
}
1118+
}
1119+
}
10891120

10901121
SuggestedFix.Builder suggestedFixBuilder = SuggestedFix.builder();
10911122
if (removeDefault) {
10921123
suggestedFixBuilder.setShortDescription(REMOVE_DEFAULT_CASE_SHORT_DESCRIPTION);
10931124
}
10941125
suggestedFixBuilder.replace(switchTree, replacementCodeBuilder.toString());
1095-
// Delete trailing statements, leaving comments where feasible
1126+
// Delete dead code, leaving comments where feasible
10961127
statementsToDelete.forEach(deleteMe -> suggestedFixBuilder.replace(deleteMe, ""));
10971128
return suggestedFixBuilder.build();
10981129
}
@@ -1131,39 +1162,6 @@ private static List<StatementTree> getPrecedingStatementsInBlock(
11311162
return precedingStatements;
11321163
}
11331164

1134-
/**
1135-
* Retrieves a list of all statements (if any) following the supplied {@code SwitchTree} in its
1136-
* lowest-ancestor statement block (if any).
1137-
*/
1138-
private static List<StatementTree> followingStatementsInBlock(
1139-
SwitchTree switchTree, VisitorState state) {
1140-
List<StatementTree> followingStatements = new ArrayList<>();
1141-
1142-
// NOMUTANTS--for performance/early return only; correctness unchanged
1143-
if (!Matchers.nextStatement(Matchers.<StatementTree>anything()).matches(switchTree, state)) {
1144-
// No lowest-ancestor block or no following statements
1145-
return followingStatements;
1146-
}
1147-
1148-
// Fetch the lowest ancestor statement block
1149-
TreePath pathToEnclosing = state.findPathToEnclosing(BlockTree.class);
1150-
// NOMUTANTS--should early return above
1151-
if (pathToEnclosing != null) {
1152-
Tree enclosing = pathToEnclosing.getLeaf();
1153-
if (enclosing instanceof BlockTree blockTree) {
1154-
// Path from root -> switchTree
1155-
TreePath rootToSwitchPath = TreePath.getPath(pathToEnclosing, switchTree);
1156-
1157-
for (int i = findBlockStatementIndex(rootToSwitchPath, blockTree) + 1;
1158-
(i >= 0) && (i < blockTree.getStatements().size());
1159-
i++) {
1160-
followingStatements.add(blockTree.getStatements().get(i));
1161-
}
1162-
}
1163-
}
1164-
return followingStatements;
1165-
}
1166-
11671165
/**
11681166
* Search through the provided {@code BlockTree} to find which statement in that block tree lies
11691167
* along the supplied {@code TreePath}. Returns the index (zero-based) of the matching statement
@@ -1172,15 +1170,28 @@ private static List<StatementTree> followingStatementsInBlock(
11721170
private static int findBlockStatementIndex(TreePath treePath, BlockTree blockTree) {
11731171
for (int i = 0; i < blockTree.getStatements().size(); i++) {
11741172
StatementTree thisStatement = blockTree.getStatements().get(i);
1175-
// Look for thisStatement along the path from the root to the switch tree
1176-
TreePath pathFromRootToThisStatement = TreePath.getPath(treePath, thisStatement);
1177-
if (pathFromRootToThisStatement != null) {
1173+
if (findTreeInPath(treePath, thisStatement)) {
11781174
return i;
11791175
}
11801176
}
11811177
return -1;
11821178
}
11831179

1180+
/**
1181+
* Search through the provided {@code TreePath} to find the provided {@code Tree} along it.
1182+
* Returns true if found, false otherwise.
1183+
*/
1184+
private static boolean findTreeInPath(TreePath treePath, Tree tree) {
1185+
for (TreePath currentPath = treePath;
1186+
currentPath != null;
1187+
currentPath = currentPath.getParentPath()) {
1188+
if (currentPath.getLeaf() == tree) {
1189+
return true;
1190+
}
1191+
}
1192+
return false;
1193+
}
1194+
11841195
/**
11851196
* Determines whether the last two preceding statements are not variable declarations within the
11861197
* same VariableDeclaratorList, for example {@code int x, y;}. VariableDeclaratorLists are defined

0 commit comments

Comments
 (0)