Skip to content

Commit 53bea60

Browse files
markhbradyError Prone Team
authored and
Error Prone Team
committed
[StatementSwitchToExpressionSwitch] for the return switch pattern, fix a bug where the auto-fix can contain dead code, which will lead to a compile-time error if adopted.
PiperOrigin-RevId: 735958054
1 parent 52a3318 commit 53bea60

File tree

3 files changed

+309
-50
lines changed

3 files changed

+309
-50
lines changed

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

+85-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(/* patches= */ ImmutableMap.of()), null);
71+
}
72+
73+
/**
74+
* Returns {@code true} if the given statement can complete normally, as defined by JLS 14.21,
75+
* when taking into account the given {@code patches}. The patches are a (possibly empty) map from
76+
* {@code Tree} to a boolean indicating whether that specific {@code Tree} can complete normally.
77+
* All relevant tree(s) not present in the patches will be analyzed as per the JLS.
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
}
@@ -118,6 +143,9 @@ private boolean scan(Tree tree) {
118143
/* A break statement cannot complete normally. */
119144
@Override
120145
public Boolean visitBreak(BreakTree tree, Void unused) {
146+
if (patches.containsKey(tree)) {
147+
return patches.get(tree);
148+
}
121149
breaks.add(skipLabel(requireNonNull(((JCTree.JCBreak) tree).target)));
122150
return false;
123151
}
@@ -126,6 +154,9 @@ public Boolean visitBreak(BreakTree tree, Void unused) {
126154
@Override
127155
public Boolean visitContinue(ContinueTree tree, Void unused) {
128156
continues.add(skipLabel(requireNonNull(((JCTree.JCContinue) tree).target)));
157+
if (patches.containsKey(tree)) {
158+
return patches.get(tree);
159+
}
129160
return false;
130161
}
131162

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

136167
@Override
137168
public Boolean visitBlock(BlockTree tree, Void unused) {
169+
if (patches.containsKey(tree)) {
170+
return patches.get(tree);
171+
}
138172
return scan(tree.getStatements());
139173
}
140174

141175
/* A local class declaration statement can complete normally iff it is reachable. */
142176
@Override
143177
public Boolean visitClass(ClassTree tree, Void unused) {
178+
if (patches.containsKey(tree)) {
179+
return patches.get(tree);
180+
}
144181
return true;
145182
}
146183

147184
/* A local variable declaration statement can complete normally iff it is reachable. */
148185
@Override
149186
public Boolean visitVariable(VariableTree tree, Void unused) {
187+
if (patches.containsKey(tree)) {
188+
return patches.get(tree);
189+
}
150190
return true;
151191
}
152192

153193
/* An empty statement can complete normally iff it is reachable. */
154194
@Override
155195
public Boolean visitEmptyStatement(EmptyStatementTree tree, Void unused) {
196+
if (patches.containsKey(tree)) {
197+
return patches.get(tree);
198+
}
156199
return true;
157200
}
158201

159202
@Override
160203
public Boolean visitLabeledStatement(LabeledStatementTree tree, Void unused) {
204+
if (patches.containsKey(tree)) {
205+
return patches.get(tree);
206+
}
161207
// break/continue targets have already been resolved by javac, so
162208
// there's nothing to do here
163209
return scan(tree.getStatement());
@@ -166,6 +212,9 @@ public Boolean visitLabeledStatement(LabeledStatementTree tree, Void unused) {
166212
/* An expression statement can complete normally iff it is reachable. */
167213
@Override
168214
public Boolean visitExpressionStatement(ExpressionStatementTree tree, Void unused) {
215+
if (patches.containsKey(tree)) {
216+
return patches.get(tree);
217+
}
169218
if (isSystemExit(tree.getExpression())) {
170219
// The spec doesn't have any special handling for {@code System.exit}, but in practice it
171220
// cannot complete normally
@@ -198,6 +247,9 @@ private static boolean isSystemExit(ExpressionTree expression) {
198247
*/
199248
@Override
200249
public Boolean visitIf(IfTree tree, Void unused) {
250+
if (patches.containsKey(tree)) {
251+
return patches.get(tree);
252+
}
201253
boolean thenCompletes = scan(tree.getThenStatement());
202254
boolean elseCompletes = tree.getElseStatement() == null || scan(tree.getElseStatement());
203255
return thenCompletes || elseCompletes;
@@ -206,6 +258,9 @@ public Boolean visitIf(IfTree tree, Void unused) {
206258
/* An assert statement can complete normally iff it is reachable. */
207259
@Override
208260
public Boolean visitAssert(AssertTree tree, Void unused) {
261+
if (patches.containsKey(tree)) {
262+
return patches.get(tree);
263+
}
209264
return true;
210265
}
211266

@@ -232,6 +287,10 @@ public Boolean visitAssert(AssertTree tree, Void unused) {
232287
*/
233288
@Override
234289
public Boolean visitSwitch(SwitchTree tree, Void unused) {
290+
if (patches.containsKey(tree)) {
291+
return patches.get(tree);
292+
}
293+
235294
// (1)
236295
if (tree.getCases().stream().noneMatch(c -> isSwitchDefault(c))) {
237296
return true;
@@ -294,6 +353,9 @@ public Boolean visitSwitch(SwitchTree tree, Void unused) {
294353
*/
295354
@Override
296355
public Boolean visitWhileLoop(WhileLoopTree tree, Void unused) {
356+
if (patches.containsKey(tree)) {
357+
return patches.get(tree);
358+
}
297359
Boolean condValue = ASTHelpers.constValue(tree.getCondition(), Boolean.class);
298360
if (!Objects.equals(condValue, false)) {
299361
scan(tree.getStatement());
@@ -333,6 +395,9 @@ public Boolean visitWhileLoop(WhileLoopTree tree, Void unused) {
333395
*/
334396
@Override
335397
public Boolean visitDoWhileLoop(DoWhileLoopTree that, Void unused) {
398+
if (patches.containsKey(that)) {
399+
return patches.get(that);
400+
}
336401
boolean completes = scan(that.getStatement());
337402
boolean conditionIsAlwaysTrue =
338403
firstNonNull(ASTHelpers.constValue(that.getCondition(), Boolean.class), false);
@@ -367,6 +432,9 @@ public Boolean visitDoWhileLoop(DoWhileLoopTree that, Void unused) {
367432
*/
368433
@Override
369434
public Boolean visitForLoop(ForLoopTree that, Void unused) {
435+
if (patches.containsKey(that)) {
436+
return patches.get(that);
437+
}
370438
Boolean condValue = ASTHelpers.constValue(that.getCondition(), Boolean.class);
371439
if (!Objects.equals(condValue, false)) {
372440
scan(that.getStatement());
@@ -385,19 +453,28 @@ public Boolean visitForLoop(ForLoopTree that, Void unused) {
385453
/* An enhanced for statement can complete normally iff it is reachable. */
386454
@Override
387455
public Boolean visitEnhancedForLoop(EnhancedForLoopTree that, Void unused) {
456+
if (patches.containsKey(that)) {
457+
return patches.get(that);
458+
}
388459
scan(that.getStatement());
389460
return true;
390461
}
391462

392463
/* A return statement cannot complete normally. */
393464
@Override
394465
public Boolean visitReturn(ReturnTree tree, Void unused) {
466+
if (patches.containsKey(tree)) {
467+
return patches.get(tree);
468+
}
395469
return false;
396470
}
397471

398472
/* A throw statement cannot complete normally. */
399473
@Override
400474
public Boolean visitThrow(ThrowTree tree, Void unused) {
475+
if (patches.containsKey(tree)) {
476+
return patches.get(tree);
477+
}
401478
return false;
402479
}
403480

@@ -410,6 +487,9 @@ public Boolean visitThrow(ThrowTree tree, Void unused) {
410487
*/
411488
@Override
412489
public Boolean visitSynchronized(SynchronizedTree tree, Void unused) {
490+
if (patches.containsKey(tree)) {
491+
return patches.get(tree);
492+
}
413493
return scan(tree.getBlock());
414494
}
415495

@@ -424,6 +504,9 @@ public Boolean visitSynchronized(SynchronizedTree tree, Void unused) {
424504
*/
425505
@Override
426506
public Boolean visitTry(TryTree that, Void unused) {
507+
if (patches.containsKey(that)) {
508+
return patches.get(that);
509+
}
427510
boolean completes = scan(that.getBlock());
428511
// assume all catch blocks are reachable; javac has already rejected unreachable
429512
// checked exception handlers

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

+35-47
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static com.google.common.collect.ImmutableSet.toImmutableSet;
2121
import static com.google.common.collect.Iterables.getLast;
2222
import static com.google.common.collect.Iterables.getOnlyElement;
23+
import static com.google.common.collect.Streams.stream;
2324
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
2425
import static com.google.errorprone.matchers.Description.NO_MATCH;
2526
import static com.google.errorprone.util.ASTHelpers.getStartPosition;
@@ -39,6 +40,7 @@
3940
import com.google.common.collect.HashBiMap;
4041
import com.google.common.collect.ImmutableBiMap;
4142
import com.google.common.collect.ImmutableList;
43+
import com.google.common.collect.ImmutableMap;
4244
import com.google.common.collect.ImmutableSet;
4345
import com.google.common.collect.Iterables;
4446
import com.google.common.collect.Streams;
@@ -1082,17 +1084,43 @@ private static SuggestedFix convertToReturnSwitch(
10821084
// Close the switch statement
10831085
replacementCodeBuilder.append("\n};");
10841086

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));
1087+
// The transformed code can cause other existing code to become dead code. So, we must analyze
1088+
// and delete such dead code, otherwise the suggested autofix could fail to compile.
1089+
1090+
// The `return switch ...` will always return or throw
1091+
Tree cannotCompleteNormallyTree = switchTree;
1092+
// Search up the AST for enclosing statement blocks, marking any newly-dead code for deletion
1093+
// along the way
1094+
for (Tree tree : state.getPath()) {
1095+
if (tree instanceof BlockTree blockTree) {
1096+
TreePath rootToCurrentPath = TreePath.getPath(state.getPath(), switchTree);
1097+
int indexInBlock = findBlockStatementIndex(rootToCurrentPath, blockTree);
1098+
// A single mock of the immediate child statement block (or switch) is sufficient to
1099+
// analyze reachability here; deeper-nested statements are not relevant.
1100+
boolean nextStatementReachable =
1101+
Reachability.canCompleteNormally(
1102+
blockTree.getStatements().get(indexInBlock),
1103+
ImmutableMap.of(cannotCompleteNormallyTree, false));
1104+
// If we continue to the ancestor statement block, it will be because the end of this
1105+
// statement block is not reachable
1106+
cannotCompleteNormallyTree = blockTree;
1107+
if (nextStatementReachable) {
1108+
break;
1109+
}
1110+
1111+
// If the next statement is not reachable, then none of the following statements in this
1112+
// block are either. So, we need to delete them all.
1113+
statementsToDelete.addAll(
1114+
blockTree.getStatements().subList(indexInBlock + 1, blockTree.getStatements().size()));
1115+
}
1116+
}
10891117

10901118
SuggestedFix.Builder suggestedFixBuilder = SuggestedFix.builder();
10911119
if (removeDefault) {
10921120
suggestedFixBuilder.setShortDescription(REMOVE_DEFAULT_CASE_SHORT_DESCRIPTION);
10931121
}
10941122
suggestedFixBuilder.replace(switchTree, replacementCodeBuilder.toString());
1095-
// Delete trailing statements, leaving comments where feasible
1123+
// Delete dead code, leaving comments where feasible
10961124
statementsToDelete.forEach(deleteMe -> suggestedFixBuilder.replace(deleteMe, ""));
10971125
return suggestedFixBuilder.build();
10981126
}
@@ -1131,54 +1159,14 @@ private static List<StatementTree> getPrecedingStatementsInBlock(
11311159
return precedingStatements;
11321160
}
11331161

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-
11671162
/**
11681163
* Search through the provided {@code BlockTree} to find which statement in that block tree lies
11691164
* along the supplied {@code TreePath}. Returns the index (zero-based) of the matching statement
11701165
* in the block tree, or -1 if not found.
11711166
*/
11721167
private static int findBlockStatementIndex(TreePath treePath, BlockTree blockTree) {
1173-
for (int i = 0; i < blockTree.getStatements().size(); i++) {
1174-
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) {
1178-
return i;
1179-
}
1180-
}
1181-
return -1;
1168+
return Iterables.indexOf(
1169+
blockTree.getStatements(), stmt -> stream(treePath).anyMatch(t -> t == stmt));
11821170
}
11831171

11841172
/**

0 commit comments

Comments
 (0)