1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2024 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 * <p>
68 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
69 * </p>
70 *
71 * <p>
72 * Violation Message Keys:
73 * </p>
74 * <ul>
75 * <li>
76 * {@code missing.switch.nullcase}
77 * </li>
78 * </ul>
79 *
80 * @since 10.18.0
81 */
82
83 @StatelessCheck
84 public class MissingNullCaseInSwitchCheck extends AbstractCheck {
85
86 /**
87 * A key is pointing to the warning message text in "messages.properties"
88 * file.
89 */
90 public static final String MSG_KEY = "missing.switch.nullcase";
91
92 @Override
93 public int[] getDefaultTokens() {
94 return getRequiredTokens();
95 }
96
97 @Override
98 public int[] getAcceptableTokens() {
99 return getRequiredTokens();
100 }
101
102 @Override
103 public int[] getRequiredTokens() {
104 return new int[] {TokenTypes.LITERAL_SWITCH};
105 }
106
107 @Override
108 public void visitToken(DetailAST ast) {
109 final List<DetailAST> caseLabels = getAllCaseLabels(ast);
110 final boolean hasNullCaseLabel = caseLabels.stream()
111 .anyMatch(MissingNullCaseInSwitchCheck::hasLiteralNull);
112 if (!hasNullCaseLabel) {
113 final boolean hasPatternCaseLabel = caseLabels.stream()
114 .anyMatch(MissingNullCaseInSwitchCheck::hasPatternCaseLabel);
115 final boolean hasStringCaseLabel = caseLabels.stream()
116 .anyMatch(MissingNullCaseInSwitchCheck::hasStringCaseLabel);
117 if (hasPatternCaseLabel || hasStringCaseLabel) {
118 log(ast, MSG_KEY);
119 }
120 }
121 }
122
123 /**
124 * Gets all case labels in the given switch AST node.
125 *
126 * @param switchAST the AST node representing {@code LITERAL_SWITCH}
127 * @return a list of all case labels in the switch
128 */
129 private static List<DetailAST> getAllCaseLabels(DetailAST switchAST) {
130 final List<DetailAST> caseLabels = new ArrayList<>();
131 DetailAST ast = switchAST.getFirstChild();
132 while (ast != null) {
133 // case group token may have several LITERAL_CASE tokens
134 TokenUtil.forEachChild(ast, TokenTypes.LITERAL_CASE, caseLabels::add);
135 ast = ast.getNextSibling();
136 }
137 return Collections.unmodifiableList(caseLabels);
138 }
139
140 /**
141 * Checks if the given case AST node has a null label.
142 *
143 * @param caseAST the AST node representing {@code LITERAL_CASE}
144 * @return true if the case has {@code null} label, false otherwise
145 */
146 private static boolean hasLiteralNull(DetailAST caseAST) {
147 return Optional.ofNullable(caseAST.findFirstToken(TokenTypes.EXPR))
148 .map(exp -> exp.findFirstToken(TokenTypes.LITERAL_NULL))
149 .isPresent();
150 }
151
152 /**
153 * Checks if the given case AST node has a pattern variable declaration label
154 * or record pattern definition label.
155 *
156 * @param caseAST the AST node representing {@code LITERAL_CASE}
157 * @return true if case has a pattern in its label
158 */
159 private static boolean hasPatternCaseLabel(DetailAST caseAST) {
160 return caseAST.findFirstToken(TokenTypes.RECORD_PATTERN_DEF) != null
161 || caseAST.findFirstToken(TokenTypes.PATTERN_VARIABLE_DEF) != null
162 || caseAST.findFirstToken(TokenTypes.PATTERN_DEF) != null;
163 }
164
165 /**
166 * Checks if the given case contains a string in its label.
167 * It may contain a single string literal or a string literal
168 * in a concatenated expression.
169 *
170 * @param caseAST the AST node representing {@code LITERAL_CASE}
171 * @return true if switch block contains a string case label
172 */
173 private static boolean hasStringCaseLabel(DetailAST caseAST) {
174 DetailAST curNode = caseAST;
175 boolean hasStringCaseLabel = false;
176 boolean exitCaseLabelExpression = false;
177 while (!exitCaseLabelExpression) {
178 DetailAST toVisit = curNode.getFirstChild();
179 if (curNode.getType() == TokenTypes.STRING_LITERAL) {
180 hasStringCaseLabel = true;
181 break;
182 }
183 while (toVisit == null) {
184 toVisit = curNode.getNextSibling();
185 curNode = curNode.getParent();
186 }
187 curNode = toVisit;
188 exitCaseLabelExpression = TokenUtil.isOfType(curNode, TokenTypes.COLON,
189 TokenTypes.LAMBDA);
190 }
191 return hasStringCaseLabel;
192 }
193 }