Skip to content

Commit 630bc3b

Browse files
markhbradyError Prone Team
authored andcommitted
[IfChainToSwitch] improve detection of duplicate constant values and invalid switch expression types
PiperOrigin-RevId: 938260967
1 parent 3ef6799 commit 630bc3b

2 files changed

Lines changed: 106 additions & 2 deletions

File tree

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

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
import java.util.stream.Stream;
9191
import javax.inject.Inject;
9292
import javax.lang.model.element.ElementKind;
93+
import javax.lang.model.type.TypeKind;
9394
import org.jspecify.annotations.Nullable;
9495

9596
/** Checks for chains of if statements that may be converted to a switch. */
@@ -113,6 +114,9 @@ public final class IfChainToSwitch extends BugChecker implements IfTreeMatcher {
113114
"java.lang.Integer",
114115
"java.lang.String");
115116

117+
// Non-reference types that are allowed for switch subjects, as specified in JLS 21 §14.11.
118+
private static final ImmutableSet<TypeKind> ALLOWED_SWITCH_SUBJECT_NON_REFERENCE_TYPES =
119+
ImmutableSet.of(TypeKind.CHAR, TypeKind.BYTE, TypeKind.SHORT, TypeKind.INT);
116120
private final boolean enableMain;
117121
private final boolean enableSafe;
118122
private final int maxChainLength;
@@ -729,6 +733,18 @@ private static Optional<List<CaseIr>> maybeDetectDuplicateConstants(List<CaseIr>
729733
// Check for compile-time constants
730734
Object constant = constValue(expression);
731735
if (constant != null) {
736+
// Canonicialize constants (by widening) to detect identical values across various
737+
// (boxed) primitive types
738+
constant =
739+
switch (constant) {
740+
case Float f -> (double) f;
741+
case Double d -> d;
742+
case Number n -> n.longValue(); // primitive integer type
743+
case Boolean bool -> bool;
744+
case Character c -> (long) c;
745+
default -> constant;
746+
};
747+
732748
if (seenConstants.contains(constant)) {
733749
return Optional.empty();
734750
}
@@ -1412,6 +1428,20 @@ private Optional<ExpressionTree> validateCompileTimeConstantForSubject(
14121428
return Optional.empty();
14131429
}
14141430

1431+
// The variable being switched on must also be one of the following types; this is
1432+
// an outer bound (relaxed in preview features, which this checker does not yet support)
1433+
if (subject.isPresent()) {
1434+
var switchExpressionType = getType(subject.get());
1435+
boolean validSwitchExpressionType =
1436+
switchExpressionType != null // Should never be null
1437+
&& (switchExpressionType.isReference()
1438+
|| ALLOWED_SWITCH_SUBJECT_NON_REFERENCE_TYPES.contains(
1439+
switchExpressionType.getKind()));
1440+
if (!validSwitchExpressionType) {
1441+
return Optional.empty();
1442+
}
1443+
}
1444+
14151445
boolean addDefault = hasElse && !hasElseIf;
14161446
int previousCaseEndPosition =
14171447
cases.isEmpty()
@@ -1425,7 +1455,7 @@ private Optional<ExpressionTree> validateCompileTimeConstantForSubject(
14251455
/* guardOptional= */ Optional.empty(),
14261456
/* expressionsOptional= */ Optional.of(ImmutableList.of(compileTimeConstant)),
14271457
/* arrowRhsOptional= */ arrowRhsOptional,
1428-
/* caseSourceCodeRange= */ Range.openClosed(previousCaseEndPosition, caseEndPosition)));
1458+
/* caseSourceCodeRange= */ Range.closedOpen(previousCaseEndPosition, caseEndPosition)));
14291459
if (addDefault) {
14301460
previousCaseEndPosition =
14311461
cases.isEmpty()
@@ -1439,7 +1469,7 @@ private Optional<ExpressionTree> validateCompileTimeConstantForSubject(
14391469
/* guardOptional= */ Optional.empty(),
14401470
/* expressionsOptional= */ Optional.empty(),
14411471
/* arrowRhsOptional= */ elseOptional,
1442-
/* caseSourceCodeRange= */ Range.openClosed(
1472+
/* caseSourceCodeRange= */ Range.closedOpen(
14431473
previousCaseEndPosition,
14441474
elseOptional.isPresent()
14451475
? getStartPosition(elseOptional.get())

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,32 @@ public void foo(Suit s) {
314314
.doTest();
315315
}
316316

317+
@Test
318+
public void ifChain_longSwitchExpression_noError() {
319+
// Similar to ifChain_longSwitch_noError, but constants are ints instead of longs
320+
helper
321+
.addSourceLines(
322+
"Test.java",
323+
"""
324+
import java.lang.Number;
325+
326+
class Test {
327+
public void foo(Suit s) {
328+
long l = s == null ? 1 : 2;
329+
if (l == 23) {
330+
System.out.println("It's 23");
331+
} else if (l == 45) {
332+
System.out.println("It's 45");
333+
} else if (l == 67) {
334+
System.out.println("It's 67");
335+
}
336+
}
337+
}
338+
""")
339+
.setArgs(ENABLE_MAIN, MIN_CHAIN_LENGTH_3)
340+
.doTest();
341+
}
342+
317343
@Test
318344
public void ifChain_intSwitch_error() {
319345
// Switch on int is supported
@@ -4440,6 +4466,54 @@ public void foo(Object o) {
44404466
.doTest();
44414467
}
44424468

4469+
@Test
4470+
public void ifChain_intDuplicateConstantDifferentTypes_fails() {
4471+
// Because 'a' is encoded as 97 (in ASCII), they are both the same (when cast to int)
4472+
refactoringHelper
4473+
.addInputLines(
4474+
"Test.java",
4475+
"""
4476+
class Test {
4477+
public void foo(int x) {
4478+
if (x == 'a') {
4479+
System.out.println("a");
4480+
} else if (x == 97) {
4481+
System.out.println("97");
4482+
} else if (x == 98) {
4483+
System.out.println("98");
4484+
}
4485+
}
4486+
}
4487+
""")
4488+
.expectUnchanged()
4489+
.setArgs(ENABLE_MAIN, MIN_CHAIN_LENGTH_3)
4490+
.doTest();
4491+
}
4492+
4493+
@Test
4494+
public void ifChain_shortDuplicateConstantDifferentTypes_fails() {
4495+
// Because 'a' is encoded as 97 (in ASCII), they are both the same (when cast to short)
4496+
refactoringHelper
4497+
.addInputLines(
4498+
"Test.java",
4499+
"""
4500+
class Test {
4501+
public void foo(short x) {
4502+
if (x == 'a') {
4503+
System.out.println("a");
4504+
} else if (x == 97) {
4505+
System.out.println("97");
4506+
} else if (x == 98) {
4507+
System.out.println("98");
4508+
}
4509+
}
4510+
}
4511+
""")
4512+
.expectUnchanged()
4513+
.setArgs(ENABLE_MAIN, MIN_CHAIN_LENGTH_3)
4514+
.doTest();
4515+
}
4516+
44434517
/** Substitute underscore for {@code unused} variables, if supported. */
44444518
private static String maybeChangeToUnnamedVariable(String s) {
44454519
if (Runtime.version().feature() >= 22) {

0 commit comments

Comments
 (0)