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