Skip to content

Commit 95621c3

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 95621c3

File tree

3 files changed

+312
-50
lines changed

3 files changed

+312
-50
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(/* 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
}
@@ -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

+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)