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}