001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.coding;
021
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.List;
025import java.util.Optional;
026
027import com.puppycrawl.tools.checkstyle.StatelessCheck;
028import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
029import com.puppycrawl.tools.checkstyle.api.DetailAST;
030import com.puppycrawl.tools.checkstyle.api.TokenTypes;
031import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
032
033/**
034 * <div>
035 * Checks that a given switch statement or expression that use a reference type in its selector
036 * expression has a {@code null} case label.
037 * </div>
038 *
039 * <p>
040 * Rationale: switch statements and expressions in Java throw a
041 * {@code NullPointerException} if the selector expression evaluates to {@code null}.
042 * As of Java 21, it is now possible to integrate a null check within the switch,
043 * eliminating the risk of {@code NullPointerException} and simplifies the code
044 * as there is no need for an external null check before entering the switch.
045 * </p>
046 *
047 * <p>
048 * See the <a href="https://docs.oracle.com/javase/specs/jls/se22/html/jls-15.html#jls-15.28">
049 * Java Language Specification</a> for more information about switch statements and expressions.
050 * </p>
051 *
052 * <p>
053 * Specifically, this check validates switch statement or expression
054 * that use patterns or strings in their case labels.
055 * </p>
056 *
057 * <p>
058 * Due to Checkstyle not being type-aware, this check cannot validate other reference types,
059 * such as enums; syntactically, these are no different from other constants.
060 * </p>
061 *
062 * <p>
063 * <b>Attention</b>: this Check should be activated only on source code
064 * that is compiled by jdk21 or above.
065 * </p>
066 *
067 * <p>
068 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
069 * </p>
070 *
071 * <p>
072 * Violation Message Keys:
073 * </p>
074 * <ul>
075 * <li>
076 * {@code missing.switch.nullcase}
077 * </li>
078 * </ul>
079 *
080 * @since 10.18.0
081 */
082
083@StatelessCheck
084public class MissingNullCaseInSwitchCheck extends AbstractCheck {
085
086    /**
087     * A key is pointing to the warning message text in "messages.properties"
088     * file.
089     */
090    public static final String MSG_KEY = "missing.switch.nullcase";
091
092    @Override
093    public int[] getDefaultTokens() {
094        return getRequiredTokens();
095    }
096
097    @Override
098    public int[] getAcceptableTokens() {
099        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}