1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.coding;
21
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.List;
25 import java.util.Optional;
26
27 import com.puppycrawl.tools.checkstyle.StatelessCheck;
28 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
29 import com.puppycrawl.tools.checkstyle.api.DetailAST;
30 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
31 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
32
33 /**
34 * <div>
35 * Checks that a given switch statement or expression that use a reference type in its selector
36 * expression has a {@code null} case label.
37 * </div>
38 *
39 * <p>
40 * Rationale: switch statements and expressions in Java throw a
41 * {@code NullPointerException} if the selector expression evaluates to {@code null}.
42 * As of Java 21, it is now possible to integrate a null check within the switch,
43 * eliminating the risk of {@code NullPointerException} and simplifies the code
44 * as there is no need for an external null check before entering the switch.
45 * </p>
46 *
47 * <p>
48 * See the <a href="https://docs.oracle.com/javase/specs/jls/se22/html/jls-15.html#jls-15.28">
49 * Java Language Specification</a> for more information about switch statements and expressions.
50 * </p>
51 *
52 * <p>
53 * Specifically, this check validates switch statement or expression
54 * that use patterns or strings in their case labels.
55 * </p>
56 *
57 * <p>
58 * Due to Checkstyle not being type-aware, this check cannot validate other reference types,
59 * such as enums; syntactically, these are no different from other constants.
60 * </p>
61 *
62 * <p>
63 * <b>Attention</b>: this Check should be activated only on source code
64 * that is compiled by jdk21 or above.
65 * </p>
66 *
67 * @since 10.18.0
68 */
69
70 @StatelessCheck
71 public class MissingNullCaseInSwitchCheck extends AbstractCheck {
72
73 /**
74 * A key is pointing to the warning message text in "messages.properties"
75 * file.
76 */
77 public static final String MSG_KEY = "missing.switch.nullcase";
78
79 @Override
80 public int[] getDefaultTokens() {
81 return getRequiredTokens();
82 }
83
84 @Override
85 public int[] getAcceptableTokens() {
86 return getRequiredTokens();
87 }
88
89 @Override
90 public int[] getRequiredTokens() {
91 return new int[] {TokenTypes.LITERAL_SWITCH};
92 }
93
94 @Override
95 public void visitToken(DetailAST ast) {
96 final List<DetailAST> caseLabels = getAllCaseLabels(ast);
97 final boolean hasNullCaseLabel = caseLabels.stream()
98 .anyMatch(MissingNullCaseInSwitchCheck::hasLiteralNull);
99 if (!hasNullCaseLabel) {
100 final boolean hasPatternCaseLabel = caseLabels.stream()
101 .anyMatch(MissingNullCaseInSwitchCheck::hasPatternCaseLabel);
102 final boolean hasStringCaseLabel = caseLabels.stream()
103 .anyMatch(MissingNullCaseInSwitchCheck::hasStringCaseLabel);
104 if (hasPatternCaseLabel || hasStringCaseLabel) {
105 log(ast, MSG_KEY);
106 }
107 }
108 }
109
110 /**
111 * Gets all case labels in the given switch AST node.
112 *
113 * @param switchAST the AST node representing {@code LITERAL_SWITCH}
114 * @return a list of all case labels in the switch
115 */
116 private static List<DetailAST> getAllCaseLabels(DetailAST switchAST) {
117 final List<DetailAST> caseLabels = new ArrayList<>();
118 DetailAST ast = switchAST.getFirstChild();
119 while (ast != null) {
120 // case group token may have several LITERAL_CASE tokens
121 TokenUtil.forEachChild(ast, TokenTypes.LITERAL_CASE, caseLabels::add);
122 ast = ast.getNextSibling();
123 }
124 return Collections.unmodifiableList(caseLabels);
125 }
126
127 /**
128 * Checks if the given case AST node has a null label.
129 *
130 * @param caseAST the AST node representing {@code LITERAL_CASE}
131 * @return true if the case has {@code null} label, false otherwise
132 */
133 private static boolean hasLiteralNull(DetailAST caseAST) {
134 return Optional.ofNullable(caseAST.findFirstToken(TokenTypes.EXPR))
135 .map(exp -> exp.findFirstToken(TokenTypes.LITERAL_NULL))
136 .isPresent();
137 }
138
139 /**
140 * Checks if the given case AST node has a pattern variable declaration label
141 * or record pattern definition label.
142 *
143 * @param caseAST the AST node representing {@code LITERAL_CASE}
144 * @return true if case has a pattern in its label
145 */
146 private static boolean hasPatternCaseLabel(DetailAST caseAST) {
147 return caseAST.findFirstToken(TokenTypes.RECORD_PATTERN_DEF) != null
148 || caseAST.findFirstToken(TokenTypes.PATTERN_VARIABLE_DEF) != null
149 || caseAST.findFirstToken(TokenTypes.PATTERN_DEF) != null;
150 }
151
152 /**
153 * Checks if the given case contains a string in its label.
154 * It may contain a single string literal or a string literal
155 * in a concatenated expression.
156 *
157 * @param caseAST the AST node representing {@code LITERAL_CASE}
158 * @return true if switch block contains a string case label
159 */
160 private static boolean hasStringCaseLabel(DetailAST caseAST) {
161 DetailAST curNode = caseAST;
162 boolean hasStringCaseLabel = false;
163 boolean exitCaseLabelExpression = false;
164 while (!exitCaseLabelExpression) {
165 DetailAST toVisit = curNode.getFirstChild();
166 if (curNode.getType() == TokenTypes.STRING_LITERAL) {
167 hasStringCaseLabel = true;
168 break;
169 }
170 while (toVisit == null) {
171 toVisit = curNode.getNextSibling();
172 curNode = curNode.getParent();
173 }
174 curNode = toVisit;
175 exitCaseLabelExpression = TokenUtil.isOfType(curNode, TokenTypes.COLON,
176 TokenTypes.LAMBDA);
177 }
178 return hasStringCaseLabel;
179 }
180 }