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