2121import static com .google .common .collect .ImmutableSet .toImmutableSet ;
2222import static com .google .errorprone .BugPattern .SeverityLevel .WARNING ;
2323import static com .google .errorprone .bugpatterns .SwitchUtils .COMPILE_TIME_CONSTANT_MATCHER ;
24+ import static com .google .errorprone .bugpatterns .SwitchUtils .getReferencedLocalVariablesInTree ;
2425import static com .google .errorprone .bugpatterns .SwitchUtils .isEnumValue ;
2526import static com .google .errorprone .bugpatterns .SwitchUtils .renderComments ;
2627import static com .google .errorprone .matchers .Description .NO_MATCH ;
2728import static com .google .errorprone .util .ASTHelpers .constValue ;
2829import static com .google .errorprone .util .ASTHelpers .getStartPosition ;
2930import static com .google .errorprone .util .ASTHelpers .getType ;
31+ import static com .google .errorprone .util .ASTHelpers .isConsideredFinal ;
3032import static com .google .errorprone .util .ASTHelpers .isSubtype ;
3133import static com .google .errorprone .util .ASTHelpers .sameVariable ;
3234import static com .sun .source .tree .Tree .Kind .EXPRESSION_STATEMENT ;
4850import com .google .errorprone .fixes .SuggestedFix ;
4951import com .google .errorprone .fixes .SuggestedFixes ;
5052import com .google .errorprone .matchers .Description ;
51- import com .google .errorprone .suppliers .Suppliers ;
5253import com .google .errorprone .util .ASTHelpers ;
5354import com .google .errorprone .util .ErrorProneComment ;
5455import com .google .errorprone .util .Reachability ;
6061import com .sun .source .tree .ExpressionTree ;
6162import com .sun .source .tree .IfTree ;
6263import com .sun .source .tree .InstanceOfTree ;
64+ import com .sun .source .tree .LiteralTree ;
6365import com .sun .source .tree .StatementTree ;
6466import com .sun .source .tree .Tree ;
6567import com .sun .source .tree .Tree .Kind ;
@@ -90,6 +92,19 @@ public final class IfChainToSwitch extends BugChecker implements IfTreeMatcher {
9092 // it's either an ExpressionStatement or a Throw. Refer to JLS 14 §14.11.1
9193 private static final ImmutableSet <Kind > KINDS_CONVERTIBLE_WITHOUT_BRACES =
9294 ImmutableSet .of (THROW , EXPRESSION_STATEMENT );
95+ // Types that are allowed for CaseConstant expressions to be assignable to in a switch, as
96+ // specified in JLS 21 §14.11.1.
97+ private static final ImmutableSet <String > ALLOWED_SWITCH_CASE_CONSTANT_TYPES =
98+ ImmutableSet .of (
99+ "char" ,
100+ "byte" ,
101+ "short" ,
102+ "int" ,
103+ "java.lang.Character" ,
104+ "java.lang.Byte" ,
105+ "java.lang.Short" ,
106+ "java.lang.Integer" ,
107+ "java.lang.String" );
93108
94109 private final boolean enableMain ;
95110 private final boolean enableSafe ;
@@ -917,6 +932,7 @@ private Optional<ExpressionTree> validatePredicateForSubject(
917932 ExpressionTree rhs = binaryTree .getRightOperand ();
918933 boolean predicateIsEquality = binaryTree .getKind ().equals (Kind .EQUAL_TO );
919934 boolean predicateIsConditionalAnd = binaryTree .getKind ().equals (Kind .CONDITIONAL_AND );
935+ boolean predicateIsConditionalOr = binaryTree .getKind ().equals (Kind .CONDITIONAL_OR );
920936
921937 if (!mustBeInstanceOf && predicateIsEquality ) {
922938 // Either lhs or rhs must be a compile-time constant.
@@ -954,39 +970,104 @@ private Optional<ExpressionTree> validatePredicateForSubject(
954970
955971 return Optional .empty ();
956972 }
957- } else if (predicateIsConditionalAnd ) {
973+ } else if (predicateIsConditionalAnd && ! mustBeInstanceOf ) {
958974 // Maybe the predicate is something like `a instanceof Foo && predicate`. If so, recurse on
959975 // the left side, and attach the right side of the conditional and as a guard to the
960976 // resulting case.
961- if (!mustBeInstanceOf && binaryTree .getKind ().equals (Kind .CONDITIONAL_AND )) {
962- int currentCasesSize = cases .size ();
963- var rv =
964- validatePredicateForSubject (
965- binaryTree .getLeftOperand (),
966- subject ,
967- state ,
968- /* mustBeInstanceOf= */ true ,
969- cases ,
970- elseOptional ,
977+ int currentCasesSize = cases .size ();
978+ var rv =
979+ validatePredicateForSubject (
980+ binaryTree .getLeftOperand (),
981+ subject ,
982+ state ,
983+ /* mustBeInstanceOf= */ true ,
984+ cases ,
985+ elseOptional ,
986+ arrowRhsOptional ,
987+ handledEnumValues ,
988+ ifTreeRange ,
989+ /* caseStartPosition= */ caseStartPosition );
990+ if (rv .isPresent ()) {
991+ CaseIr oldLastCase = cases .get (currentCasesSize );
992+ ExpressionTree rightOperandNoParentheses =
993+ ASTHelpers .stripParentheses (binaryTree .getRightOperand ());
994+ // A guard cannot just be `false` (not valid Java)
995+ if (isBooleanLiteral (rightOperandNoParentheses )
996+ && rightOperandNoParentheses instanceof LiteralTree literalTree
997+ && literalTree .getValue () instanceof Boolean b
998+ && !b ) {
999+ return Optional .empty ();
1000+ }
1001+ // A guard cannot reference a local variable that is not final nor effectively final;
1002+ // see JLS 21 §14.11.1.
1003+ if (getReferencedLocalVariablesInTree (rightOperandNoParentheses ).stream ()
1004+ .anyMatch (varSymbol -> !isConsideredFinal (varSymbol ))) {
1005+ return Optional .empty ();
1006+ }
1007+ // Update last case to attach the guard
1008+ cases .set (
1009+ currentCasesSize ,
1010+ new CaseIr (
1011+ /* hasCaseNull= */ oldLastCase .hasCaseNull (),
1012+ /* hasDefault= */ oldLastCase .hasDefault (),
1013+ /* instanceOfOptional= */ oldLastCase .instanceOfOptional (),
1014+ /* guardOptional= */ Optional .of (binaryTree .getRightOperand ()),
1015+ /* expressionsOptional= */ oldLastCase .expressionsOptional (),
1016+ /* arrowRhsOptional= */ oldLastCase .arrowRhsOptional (),
1017+ /* caseSourceCodeRange= */ oldLastCase .caseSourceCodeRange ()));
1018+ return rv ;
1019+ }
1020+
1021+ } else if (!mustBeInstanceOf && predicateIsConditionalOr ) {
1022+ // Maybe the predicate is something like `x == 1 || x == 2`.
1023+ int initialCasesSize = cases .size ();
1024+ Optional <SubjectAndCaseExpressions > orExpressionsOptional =
1025+ validateConditionalOrsForSubject (
1026+ binaryTree ,
1027+ subject ,
1028+ cases ,
1029+ elseOptional ,
1030+ arrowRhsOptional ,
1031+ ifTreeRange ,
1032+ caseStartPosition ,
1033+ caseEndPosition ,
1034+ hasElse ,
1035+ hasElseIf ,
1036+ handledEnumValues ,
1037+ state );
1038+ if (orExpressionsOptional .isPresent ()) {
1039+ SubjectAndCaseExpressions orExpressions = orExpressionsOptional .get ();
1040+ // Remove individual cases added, and add a single grouped case covering all of them
1041+ cases .subList (initialCasesSize , cases .size ()).clear ();
1042+ cases .add (
1043+ new CaseIr (
1044+ /* hasCaseNull= */ false ,
1045+ /* hasDefault= */ false ,
1046+ /* instanceOfOptional= */ Optional .empty (),
1047+ /* guardOptional= */ Optional .empty (),
1048+ Optional .of (orExpressions .expressions ()),
9711049 arrowRhsOptional ,
972- handledEnumValues ,
973- ifTreeRange ,
974- /* caseStartPosition= */ caseStartPosition );
975- if (rv .isPresent ()) {
976- CaseIr oldLastCase = cases .get (currentCasesSize );
977- // Update last case to attach the guard
978- cases .set (
979- currentCasesSize ,
1050+ /* caseSourceCodeRange= */ Range .closedOpen (caseStartPosition , caseEndPosition )));
1051+
1052+ boolean addDefault = hasElse && !hasElseIf ;
1053+ if (addDefault ) {
1054+ cases .add (
9801055 new CaseIr (
981- /* hasCaseNull= */ oldLastCase .hasCaseNull (),
982- /* hasDefault= */ oldLastCase .hasDefault (),
983- /* instanceOfOptional= */ oldLastCase .instanceOfOptional (),
984- /* guardOptional= */ Optional .of (binaryTree .getRightOperand ()),
985- /* expressionsOptional= */ oldLastCase .expressionsOptional (),
986- /* arrowRhsOptional= */ oldLastCase .arrowRhsOptional (),
987- /* caseSourceCodeRange= */ oldLastCase .caseSourceCodeRange ()));
988- return rv ;
1056+ /* hasCaseNull= */ false ,
1057+ /* hasDefault= */ true ,
1058+ /* instanceOfOptional= */ Optional .empty (),
1059+ /* guardOptional= */ Optional .empty (),
1060+ /* expressionsOptional= */
1061+ /* expressionsOptional= */ Optional .empty (),
1062+ /* arrowRhsOptional= */ elseOptional ,
1063+ /* caseSourceCodeRange= */ Range .closedOpen (
1064+ caseEndPosition ,
1065+ elseOptional .isPresent ()
1066+ ? getStartPosition (elseOptional .get ())
1067+ : caseEndPosition )));
9891068 }
1069+
1070+ return Optional .of (orExpressions .subject ());
9901071 }
9911072 }
9921073 }
@@ -1010,6 +1091,149 @@ private Optional<ExpressionTree> validatePredicateForSubject(
10101091 return Optional .empty ();
10111092 }
10121093
1094+ private static boolean isBooleanLiteral (ExpressionTree tree ) {
1095+ return tree .getKind () == Kind .BOOLEAN_LITERAL ;
1096+ }
1097+
1098+ /**
1099+ * Validates whether the {@code binaryTree} represents a series of conditional-ORs that can be
1100+ * converted to a single switch case having multiple expressions, returning the subject and case
1101+ * expressions if so. Otherwise, returns {@code Optional.empty()}.
1102+ */
1103+ private Optional <SubjectAndCaseExpressions > validateConditionalOrsForSubject (
1104+ BinaryTree binaryTree ,
1105+ Optional <ExpressionTree > subject ,
1106+ List <CaseIr > cases ,
1107+ Optional <StatementTree > elseOptional ,
1108+ Optional <StatementTree > arrowRhsOptional ,
1109+ Range <Integer > ifTreeRange ,
1110+ int caseStartPosition ,
1111+ int caseEndPosition ,
1112+ boolean hasElse ,
1113+ boolean hasElseIf ,
1114+ Set <String > handledEnumValues ,
1115+ VisitorState state ) {
1116+
1117+ if (!binaryTree .getKind ().equals (Kind .CONDITIONAL_OR )) {
1118+ return Optional .empty ();
1119+ }
1120+
1121+ // Logical-OR is associative, so we can disregard parentheses
1122+ ExpressionTree lhs = ASTHelpers .stripParentheses (binaryTree .getLeftOperand ());
1123+ ExpressionTree rhs = ASTHelpers .stripParentheses (binaryTree .getRightOperand ());
1124+ List <ExpressionTree > caseExpressions = new ArrayList <>();
1125+
1126+ ExpressionTree [] sides = {lhs , rhs };
1127+ for (ExpressionTree side : sides ) {
1128+ switch (side ) {
1129+ case BinaryTree bt when bt .getKind ().equals (Kind .EQUAL_TO ) -> {
1130+ // Maybe comparing to a non-null compile-time constant? (`case null` not supported here
1131+ // due to Java syntax restrictions)
1132+ if ((COMPILE_TIME_CONSTANT_MATCHER .matches (bt .getLeftOperand (), state )
1133+ && !isNull (bt .getLeftOperand ()))
1134+ || (COMPILE_TIME_CONSTANT_MATCHER .matches (bt .getRightOperand (), state )
1135+ && !isNull (bt .getRightOperand ()))) {
1136+ subject =
1137+ validateCompileTimeConstantForSubject (
1138+ bt .getLeftOperand (),
1139+ bt .getRightOperand (),
1140+ subject ,
1141+ state ,
1142+ cases ,
1143+ elseOptional ,
1144+ arrowRhsOptional ,
1145+ ifTreeRange ,
1146+ caseEndPosition ,
1147+ hasElse ,
1148+ hasElseIf );
1149+
1150+ if (subject .isEmpty ()) {
1151+ return Optional .empty ();
1152+ }
1153+
1154+ var compileTimeConstantExpression =
1155+ COMPILE_TIME_CONSTANT_MATCHER .matches (bt .getLeftOperand (), state )
1156+ ? bt .getLeftOperand ()
1157+ : bt .getRightOperand ();
1158+ caseExpressions .add (compileTimeConstantExpression );
1159+ } else {
1160+ // Maybe comparing to an enum value?
1161+ if ((isEnumValue (bt .getLeftOperand (), state )
1162+ && ASTHelpers .isEnumConstant (bt .getLeftOperand ()))
1163+ || (isEnumValue (bt .getRightOperand (), state )
1164+ && ASTHelpers .isEnumConstant (bt .getRightOperand ()))) {
1165+ subject =
1166+ validateEnumPredicateForSubject (
1167+ bt .getLeftOperand (),
1168+ bt .getRightOperand (),
1169+ subject ,
1170+ state ,
1171+ cases ,
1172+ elseOptional ,
1173+ arrowRhsOptional ,
1174+ handledEnumValues ,
1175+ ifTreeRange ,
1176+ caseEndPosition ,
1177+ hasElse ,
1178+ hasElseIf );
1179+
1180+ if (subject .isEmpty ()) {
1181+ return Optional .empty ();
1182+ }
1183+ var enumValueExpression =
1184+ isEnumValue (bt .getLeftOperand (), state )
1185+ && ASTHelpers .isEnumConstant (bt .getLeftOperand ())
1186+ ? bt .getLeftOperand ()
1187+ : bt .getRightOperand ();
1188+ caseExpressions .add (enumValueExpression );
1189+ } else {
1190+ // Unsupported
1191+ return Optional .empty ();
1192+ }
1193+ }
1194+ }
1195+
1196+ case BinaryTree bt -> {
1197+ // Maybe multiple comparisons connected by OR? e.g. `x == 1 || x == 2 || ...`
1198+ var subjectAndCaseExpressionsOptional =
1199+ validateConditionalOrsForSubject (
1200+ bt ,
1201+ subject ,
1202+ cases ,
1203+ elseOptional ,
1204+ arrowRhsOptional ,
1205+ ifTreeRange ,
1206+ caseStartPosition ,
1207+ caseEndPosition ,
1208+ hasElse ,
1209+ hasElseIf ,
1210+ handledEnumValues ,
1211+ state );
1212+
1213+ if (subjectAndCaseExpressionsOptional .isEmpty ()) {
1214+ return Optional .empty ();
1215+ }
1216+ SubjectAndCaseExpressions subjectAndCaseExpressions =
1217+ subjectAndCaseExpressionsOptional .get ();
1218+ subject = Optional .of (subjectAndCaseExpressions .subject ());
1219+ caseExpressions .addAll (subjectAndCaseExpressions .expressions ());
1220+ }
1221+ default -> {
1222+ // Unsupported
1223+ return Optional .empty ();
1224+ }
1225+ }
1226+ }
1227+
1228+ return caseExpressions .isEmpty ()
1229+ ? Optional .empty ()
1230+ : Optional .of (new SubjectAndCaseExpressions (subject .get (), caseExpressions ));
1231+ }
1232+
1233+ private static boolean isNull (ExpressionTree expression ) {
1234+ return expression .getKind () == Kind .NULL_LITERAL ;
1235+ }
1236+
10131237 /**
10141238 * Determines whether the {@code subject} expression "matches" the given {@code expression}. If
10151239 * {@code enableSafe} is true, then matching means that the subject must be referring to the same
@@ -1143,24 +1367,33 @@ private Optional<ExpressionTree> validateCompileTimeConstantForSubject(
11431367 boolean compileTimeConstantOnLhs = COMPILE_TIME_CONSTANT_MATCHER .matches (lhs , state );
11441368 ExpressionTree testExpression = compileTimeConstantOnLhs ? rhs : lhs ;
11451369 ExpressionTree compileTimeConstant = compileTimeConstantOnLhs ? lhs : rhs ;
1370+ Type compileTimeConstantType = getType (compileTimeConstant );
1371+ Type testExpressionType = getType (testExpression );
11461372
11471373 if (subject .isPresent () && !subjectMatches (subject .get (), testExpression , state )) {
11481374 // Predicate not compatible with predicate of preceding if statement
11491375 return Optional .empty ();
11501376 }
11511377
1152- // Don't support the use of Booleans as switch conditions
1153- if (isSubtype (getType ( testExpression ), Suppliers . JAVA_LANG_BOOLEAN_TYPE . get ( state ) , state )) {
1378+ // Don't support the use of String as switch conditions
1379+ if (isSubtype (testExpressionType , state . getSymtab (). stringType , state )) {
11541380 return Optional .empty ();
11551381 }
11561382
1157- // Don't support the use of String as switch conditions
1158- if (isSubtype (getType (testExpression ), state .getSymtab ().stringType , state )) {
1383+ // The compile time constant must be assignable to the type of the testExpression, which
1384+ // includes the possible use of assignment context conversions
1385+ Types types = state .getTypes ();
1386+ if (!types .isAssignable (compileTimeConstantType , testExpressionType )) {
11591387 return Optional .empty ();
11601388 }
11611389
1162- // Switching on primitive long requires later Java version (we don't currently support)
1163- if (state .getTypes ().isSameType (getType (testExpression ), state .getSymtab ().longType )) {
1390+ // As of Java 21, a CaseConstant must be assignable to one of the following types (this is an
1391+ // outer bound; the checker does not necessarily support all of these)
1392+ boolean caseConstantIsAssignable =
1393+ ALLOWED_SWITCH_CASE_CONSTANT_TYPES .stream ()
1394+ .map (state ::getTypeFromString )
1395+ .anyMatch (t -> types .isAssignable (compileTimeConstantType , t ));
1396+ if (!caseConstantIsAssignable ) {
11641397 return Optional .empty ();
11651398 }
11661399
@@ -1656,6 +1889,16 @@ record CaseIr(
16561889 }
16571890 }
16581891
1892+ /**
1893+ * Container for the subject (of an if predicate) and a (non-empty) list of expressions that can
1894+ * match that subject in the given case.
1895+ */
1896+ record SubjectAndCaseExpressions (ExpressionTree subject , List <ExpressionTree > expressions ) {
1897+ SubjectAndCaseExpressions {
1898+ checkArgument (!expressions .isEmpty ());
1899+ }
1900+ }
1901+
16591902 /** This record represents the current state of the analysis of an if-chain. */
16601903 record IfChainAnalysisState (
16611904 // The expression to be switched on (if known)
0 commit comments