View Javadoc
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 }