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   * <p>
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   * </p>
38   * <p>
39   * Rationale: switch statements and expressions in Java throw a
40   * {@code NullPointerException} if the selector expression evaluates to {@code null}.
41   * As of Java 21, it is now possible to integrate a null check within the switch,
42   * eliminating the risk of {@code NullPointerException} and simplifies the code
43   * as there is no need for an external null check before entering the switch.
44   * </p>
45   * <p>
46   * See the <a href="https://docs.oracle.com/javase/specs/jls/se22/html/jls-15.html#jls-15.28">
47   * Java Language Specification</a> for more information about switch statements and expressions.
48   * </p>
49   * <p>
50   * Specifically, this check validates switch statement or expression
51   * that use patterns or strings in their case labels.
52   * </p>
53   * <p>
54   * Due to Checkstyle not being type-aware, this check cannot validate other reference types,
55   * such as enums; syntactically, these are no different from other constants.
56   * </p>
57   * <p>
58   * <b>Attention</b>: this Check should be activated only on source code
59   * that is compiled by jdk21 or above.
60   * </p>
61   * <p>
62   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
63   * </p>
64   * <p>
65   * Violation Message Keys:
66   * </p>
67   * <ul>
68   * <li>
69   * {@code missing.switch.nullcase}
70   * </li>
71   * </ul>
72   *
73   * @since 10.18.0
74   */
75  
76  @StatelessCheck
77  public class MissingNullCaseInSwitchCheck extends AbstractCheck {
78  
79      /**
80       * A key is pointing to the warning message text in "messages.properties"
81       * file.
82       */
83      public static final String MSG_KEY = "missing.switch.nullcase";
84  
85      @Override
86      public int[] getDefaultTokens() {
87          return getRequiredTokens();
88      }
89  
90      @Override
91      public int[] getAcceptableTokens() {
92          return getRequiredTokens();
93      }
94  
95      @Override
96      public int[] getRequiredTokens() {
97          return new int[] {TokenTypes.LITERAL_SWITCH};
98      }
99  
100     @Override
101     public void visitToken(DetailAST ast) {
102         final List<DetailAST> caseLabels = getAllCaseLabels(ast);
103         final boolean hasNullCaseLabel = caseLabels.stream()
104                 .anyMatch(MissingNullCaseInSwitchCheck::hasLiteralNull);
105         if (!hasNullCaseLabel) {
106             final boolean hasPatternCaseLabel = caseLabels.stream()
107                 .anyMatch(MissingNullCaseInSwitchCheck::hasPatternCaseLabel);
108             final boolean hasStringCaseLabel = caseLabels.stream()
109                 .anyMatch(MissingNullCaseInSwitchCheck::hasStringCaseLabel);
110             if (hasPatternCaseLabel || hasStringCaseLabel) {
111                 log(ast, MSG_KEY);
112             }
113         }
114     }
115 
116     /**
117      * Gets all case labels in the given switch AST node.
118      *
119      * @param switchAST the AST node representing {@code LITERAL_SWITCH}
120      * @return a list of all case labels in the switch
121      */
122     private static List<DetailAST> getAllCaseLabels(DetailAST switchAST) {
123         final List<DetailAST> caseLabels = new ArrayList<>();
124         DetailAST ast = switchAST.getFirstChild();
125         while (ast != null) {
126             // case group token may have several LITERAL_CASE tokens
127             TokenUtil.forEachChild(ast, TokenTypes.LITERAL_CASE, caseLabels::add);
128             ast = ast.getNextSibling();
129         }
130         return Collections.unmodifiableList(caseLabels);
131     }
132 
133     /**
134      * Checks if the given case AST node has a null label.
135      *
136      * @param caseAST the AST node representing {@code LITERAL_CASE}
137      * @return true if the case has {@code null} label, false otherwise
138      */
139     private static boolean hasLiteralNull(DetailAST caseAST) {
140         return Optional.ofNullable(caseAST.findFirstToken(TokenTypes.EXPR))
141                 .map(exp -> exp.findFirstToken(TokenTypes.LITERAL_NULL))
142                 .isPresent();
143     }
144 
145     /**
146      * Checks if the given case AST node has a pattern variable declaration label
147      * or record pattern definition label.
148      *
149      * @param caseAST the AST node representing {@code LITERAL_CASE}
150      * @return true if case has a pattern in its label
151      */
152     private static boolean hasPatternCaseLabel(DetailAST caseAST) {
153         return caseAST.findFirstToken(TokenTypes.RECORD_PATTERN_DEF) != null
154                || caseAST.findFirstToken(TokenTypes.PATTERN_VARIABLE_DEF) != null
155                || caseAST.findFirstToken(TokenTypes.PATTERN_DEF) != null;
156     }
157 
158     /**
159      * Checks if the given case contains a string in its label.
160      * It may contain a single string literal or a string literal
161      * in a concatenated expression.
162      *
163      * @param caseAST the AST node representing {@code LITERAL_CASE}
164      * @return true if switch block contains a string case label
165      */
166     private static boolean hasStringCaseLabel(DetailAST caseAST) {
167         DetailAST curNode = caseAST;
168         boolean hasStringCaseLabel = false;
169         boolean exitCaseLabelExpression = false;
170         while (!exitCaseLabelExpression) {
171             DetailAST toVisit = curNode.getFirstChild();
172             if (curNode.getType() == TokenTypes.STRING_LITERAL) {
173                 hasStringCaseLabel = true;
174                 break;
175             }
176             while (toVisit == null) {
177                 toVisit = curNode.getNextSibling();
178                 curNode = curNode.getParent();
179             }
180             curNode = toVisit;
181             exitCaseLabelExpression = TokenUtil.isOfType(curNode, TokenTypes.COLON,
182                                                                         TokenTypes.LAMBDA);
183         }
184         return hasStringCaseLabel;
185     }
186 }