Skip to content

Commit c139e7f

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: 737649634
1 parent 296fb4e commit c139e7f

File tree

3 files changed

+249
-49
lines changed

3 files changed

+249
-49
lines changed

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

+25-1
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 new CanCompleteNormallyVisitor(/* patches= */ ImmutableMap.of()).scan(statement);
71+
}
72+
73+
/**
74+
* Returns whether the given statement can complete normally, as defined by JLS 14.21, when taking
75+
* into account the given {@code patches}. The patches are a (possibly empty) map from {@code
76+
* Tree} to a boolean indicating whether that specific {@code Tree} can complete normally. All
77+
* 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 new CanCompleteNormallyVisitor(patches).scan(statement);
7084
}
7185

7286
/**
@@ -100,6 +114,13 @@ 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) {
@@ -112,6 +133,9 @@ boolean scan(List<? extends StatementTree> trees) {
112133
// don't otherwise affect the result of the reachability analysis.
113134
@CanIgnoreReturnValue
114135
private boolean scan(Tree tree) {
136+
if (patches.containsKey(tree)) {
137+
return patches.get(tree);
138+
}
115139
return tree.accept(this, null);
116140
}
117141

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
/**

core/src/test/java/com/google/errorprone/bugpatterns/StatementSwitchToExpressionSwitchTest.java

+189-1
Original file line numberDiff line numberDiff line change
@@ -2675,7 +2675,8 @@ public int foo(Side side) {
26752675

26762676
@Test
26772677
public void switchByEnum_returnSwitchWithShouldNeverHappen_errorAndRemoveShouldNeverHappen() {
2678-
// The switch has a case for each enum and "should never happen" error handling
2678+
// The switch has a case for each enum and "should never happen" error handling for code after
2679+
// the switch in the same block. The "should never happen" code should be removed.
26792680
helper
26802681
.addSourceLines(
26812682
"Test.java",
@@ -2817,6 +2818,193 @@ public int foo(Side side) {
28172818
.doTest(BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH);
28182819
}
28192820

2821+
@Test
2822+
public void switchByEnum_deadCodeAnalysis_error() {
2823+
// Code after the return switch in the same block should be removed, as well as certain code in
2824+
// the parent block(s)
2825+
refactoringHelper
2826+
.addInputLines(
2827+
"Test.java",
2828+
"""
2829+
class Test {
2830+
enum Side {
2831+
HEART,
2832+
SPADE,
2833+
DIAMOND,
2834+
CLUB
2835+
};
2836+
2837+
public Test(int foo) {}
2838+
2839+
public int foo(Side side) {
2840+
System.out.println("don't delete 0");
2841+
try {
2842+
System.out.println("don't delete 1");
2843+
System.out.println("don't delete 11");
2844+
switch (side) {
2845+
case HEART:
2846+
case DIAMOND:
2847+
case SPADE:
2848+
return 1;
2849+
case CLUB:
2850+
throw new NullPointerException();
2851+
}
2852+
System.out.println("delete me");
2853+
} catch (Throwable e) {
2854+
throw new RuntimeException("rethrew");
2855+
}
2856+
2857+
// Becomes unreachable
2858+
System.out.println("uh-oh");
2859+
return -1;
2860+
}
2861+
}
2862+
""")
2863+
.addOutputLines(
2864+
"Test.java",
2865+
"""
2866+
class Test {
2867+
enum Side {
2868+
HEART,
2869+
SPADE,
2870+
DIAMOND,
2871+
CLUB
2872+
};
2873+
2874+
public Test(int foo) {}
2875+
2876+
public int foo(Side side) {
2877+
System.out.println("don't delete 0");
2878+
try {
2879+
System.out.println("don't delete 1");
2880+
System.out.println("don't delete 11");
2881+
return switch (side) {
2882+
case HEART, DIAMOND, SPADE -> 1;
2883+
case CLUB -> throw new NullPointerException();
2884+
};
2885+
2886+
} catch (Throwable e) {
2887+
throw new RuntimeException("rethrew");
2888+
}
2889+
2890+
// Becomes unreachable
2891+
2892+
}
2893+
}
2894+
""")
2895+
.setArgs("-XepOpt:StatementSwitchToExpressionSwitch:EnableReturnSwitchConversion")
2896+
.setFixChooser(StatementSwitchToExpressionSwitchTest::assertOneFixAndChoose)
2897+
.doTest(BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH);
2898+
}
2899+
2900+
@Test
2901+
public void switchByEnum_deadCodeAnalysis2_error() {
2902+
// This test case is similar to switchByEnum_deadCodeAnalysis_error, but with additional
2903+
// complexity and language elements such as synchronized(), if(), ...
2904+
refactoringHelper
2905+
.addInputLines(
2906+
"Test.java",
2907+
"""
2908+
class Test {
2909+
enum Side {
2910+
HEART,
2911+
SPADE,
2912+
DIAMOND,
2913+
CLUB
2914+
};
2915+
2916+
public Test(int foo) {}
2917+
2918+
int x = 3;
2919+
2920+
public int foo(Side side) {
2921+
if (x < 4) {
2922+
try {
2923+
System.out.println("don't delete 0");
2924+
synchronized (this) {
2925+
try {
2926+
System.out.println("don't delete 1");
2927+
System.out.println("don't delete 11");
2928+
switch (side) {
2929+
case HEART:
2930+
case DIAMOND:
2931+
case SPADE:
2932+
return 1;
2933+
case CLUB:
2934+
throw new NullPointerException();
2935+
}
2936+
System.out.println("delete me");
2937+
} catch (Throwable e) {
2938+
throw new RuntimeException("rethrew");
2939+
}
2940+
}
2941+
2942+
// Becomes unreachable
2943+
System.out.println("uh-oh");
2944+
} catch (Throwable e) {
2945+
throw new RuntimeException("rethrew");
2946+
}
2947+
// Also becomes unreachable
2948+
System.out.println("delete me too");
2949+
System.out.println("delete me three");
2950+
return 3;
2951+
}
2952+
System.out.println("I'm always reachable");
2953+
return 4;
2954+
}
2955+
}
2956+
""")
2957+
.addOutputLines(
2958+
"Test.java",
2959+
"""
2960+
class Test {
2961+
enum Side {
2962+
HEART,
2963+
SPADE,
2964+
DIAMOND,
2965+
CLUB
2966+
};
2967+
2968+
public Test(int foo) {}
2969+
2970+
int x = 3;
2971+
2972+
public int foo(Side side) {
2973+
if (x < 4) {
2974+
try {
2975+
System.out.println("don't delete 0");
2976+
synchronized (this) {
2977+
try {
2978+
System.out.println("don't delete 1");
2979+
System.out.println("don't delete 11");
2980+
return switch (side) {
2981+
case HEART, DIAMOND, SPADE -> 1;
2982+
case CLUB -> throw new NullPointerException();
2983+
};
2984+
2985+
} catch (Throwable e) {
2986+
throw new RuntimeException("rethrew");
2987+
}
2988+
}
2989+
2990+
// Becomes unreachable
2991+
2992+
} catch (Throwable e) {
2993+
throw new RuntimeException("rethrew");
2994+
}
2995+
// Also becomes unreachable
2996+
2997+
}
2998+
System.out.println("I'm always reachable");
2999+
return 4;
3000+
}
3001+
}
3002+
""")
3003+
.setArgs("-XepOpt:StatementSwitchToExpressionSwitch:EnableReturnSwitchConversion")
3004+
.setFixChooser(StatementSwitchToExpressionSwitchTest::assertOneFixAndChoose)
3005+
.doTest(BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH);
3006+
}
3007+
28203008
@Test
28213009
public void switchByEnum_returnSwitchWithAllEnumValuesAndDefault_errorRemoveDefault() {
28223010
// The return switch has a case for each enum value *and* a default case handler. This test

0 commit comments

Comments
 (0)