MissingNullCaseInSwitchCheck.java

///////////////////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
// Copyright (C) 2001-2024 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
///////////////////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle.checks.coding;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import com.puppycrawl.tools.checkstyle.StatelessCheck;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;

/**
 * <p>
 * Checks that a given switch statement or expression that use a reference type in its selector
 * expression has a {@code null} case label.
 * </p>
 * <p>
 * Rationale: switch statements and expressions in Java throw a
 * {@code NullPointerException} if the selector expression evaluates to {@code null}.
 * As of Java 21, it is now possible to integrate a null check within the switch,
 * eliminating the risk of {@code NullPointerException} and simplifies the code
 * as there is no need for an external null check before entering the switch.
 * </p>
 * <p>
 * See the <a href="https://docs.oracle.com/javase/specs/jls/se22/html/jls-15.html#jls-15.28">
 * Java Language Specification</a> for more information about switch statements and expressions.
 * </p>
 * <p>
 * Specifically, this check validates switch statement or expression
 * that use patterns or strings in their case labels.
 * </p>
 * <p>
 * Due to Checkstyle not being type-aware, this check cannot validate other reference types,
 * such as enums; syntactically, these are no different from other constants.
 * </p>
 * <p>
 * <b>Attention</b>: this Check should be activated only on source code
 * that is compiled by jdk21 or above.
 * </p>
 * <p>
 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
 * </p>
 * <p>
 * Violation Message Keys:
 * </p>
 * <ul>
 * <li>
 * {@code missing.switch.nullcase}
 * </li>
 * </ul>
 *
 * @since 10.18.0
 */

@StatelessCheck
public class MissingNullCaseInSwitchCheck extends AbstractCheck {

    /**
     * A key is pointing to the warning message text in "messages.properties"
     * file.
     */
    public static final String MSG_KEY = "missing.switch.nullcase";

    @Override
    public int[] getDefaultTokens() {
        return getRequiredTokens();
    }

    @Override
    public int[] getAcceptableTokens() {
        return getRequiredTokens();
    }

    @Override
    public int[] getRequiredTokens() {
        return new int[] {TokenTypes.LITERAL_SWITCH};
    }

    @Override
    public void visitToken(DetailAST ast) {
        final List<DetailAST> caseLabels = getAllCaseLabels(ast);
        final boolean hasNullCaseLabel = caseLabels.stream()
                .anyMatch(MissingNullCaseInSwitchCheck::hasLiteralNull);
        if (!hasNullCaseLabel) {
            final boolean hasPatternCaseLabel = caseLabels.stream()
                .anyMatch(MissingNullCaseInSwitchCheck::hasPatternCaseLabel);
            final boolean hasStringCaseLabel = caseLabels.stream()
                .anyMatch(MissingNullCaseInSwitchCheck::hasStringCaseLabel);
            if (hasPatternCaseLabel || hasStringCaseLabel) {
                log(ast, MSG_KEY);
            }
        }
    }

    /**
     * Gets all case labels in the given switch AST node.
     *
     * @param switchAST the AST node representing {@code LITERAL_SWITCH}
     * @return a list of all case labels in the switch
     */
    private static List<DetailAST> getAllCaseLabels(DetailAST switchAST) {
        final List<DetailAST> caseLabels = new ArrayList<>();
        DetailAST ast = switchAST.getFirstChild();
        while (ast != null) {
            // case group token may have several LITERAL_CASE tokens
            TokenUtil.forEachChild(ast, TokenTypes.LITERAL_CASE, caseLabels::add);
            ast = ast.getNextSibling();
        }
        return Collections.unmodifiableList(caseLabels);
    }

    /**
     * Checks if the given case AST node has a null label.
     *
     * @param caseAST the AST node representing {@code LITERAL_CASE}
     * @return true if the case has {@code null} label, false otherwise
     */
    private static boolean hasLiteralNull(DetailAST caseAST) {
        return Optional.ofNullable(caseAST.findFirstToken(TokenTypes.EXPR))
                .map(exp -> exp.findFirstToken(TokenTypes.LITERAL_NULL))
                .isPresent();
    }

    /**
     * Checks if the given case AST node has a pattern variable declaration label
     * or record pattern definition label.
     *
     * @param caseAST the AST node representing {@code LITERAL_CASE}
     * @return true if case has a pattern in its label
     */
    private static boolean hasPatternCaseLabel(DetailAST caseAST) {
        return caseAST.findFirstToken(TokenTypes.RECORD_PATTERN_DEF) != null
               || caseAST.findFirstToken(TokenTypes.PATTERN_VARIABLE_DEF) != null
               || caseAST.findFirstToken(TokenTypes.PATTERN_DEF) != null;
    }

    /**
     * Checks if the given case contains a string in its label.
     * It may contain a single string literal or a string literal
     * in a concatenated expression.
     *
     * @param caseAST the AST node representing {@code LITERAL_CASE}
     * @return true if switch block contains a string case label
     */
    private static boolean hasStringCaseLabel(DetailAST caseAST) {
        DetailAST curNode = caseAST;
        boolean hasStringCaseLabel = false;
        boolean exitCaseLabelExpression = false;
        while (!exitCaseLabelExpression) {
            DetailAST toVisit = curNode.getFirstChild();
            if (curNode.getType() == TokenTypes.STRING_LITERAL) {
                hasStringCaseLabel = true;
                break;
            }
            while (toVisit == null) {
                toVisit = curNode.getNextSibling();
                curNode = curNode.getParent();
            }
            curNode = toVisit;
            exitCaseLabelExpression = TokenUtil.isOfType(curNode, TokenTypes.COLON,
                                                                        TokenTypes.LAMBDA);
        }
        return hasStringCaseLabel;
    }
}