001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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.HashSet; 023import java.util.Objects; 024import java.util.Optional; 025import java.util.Set; 026import java.util.regex.Pattern; 027import java.util.stream.Stream; 028 029import com.puppycrawl.tools.checkstyle.StatelessCheck; 030import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 031import com.puppycrawl.tools.checkstyle.api.DetailAST; 032import com.puppycrawl.tools.checkstyle.api.TokenTypes; 033import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 034 035/** 036 * <div> 037 * Checks for fall-through in {@code switch} statements. 038 * Finds locations where a {@code case} <b>contains</b> Java code but lacks a 039 * {@code break}, {@code return}, {@code yield}, {@code throw} or {@code continue} statement. 040 * </div> 041 * 042 * <p> 043 * The check honors special comments to suppress the warning. 044 * By default, the texts 045 * "fallthru", "fall thru", "fall-thru", 046 * "fallthrough", "fall through", "fall-through" 047 * "fallsthrough", "falls through", "falls-through" (case-sensitive). 048 * The comment containing these words must be all on one line, 049 * and must be on the last non-empty line before the {@code case} triggering 050 * the warning or on the same line before the {@code case}(ugly, but possible). 051 * Any other comment may follow on the same line. 052 * </p> 053 * 054 * <p> 055 * Note: The check assumes that there is no unreachable code in the {@code case}. 056 * </p> 057 * 058 * @since 3.4 059 */ 060@StatelessCheck 061public class FallThroughCheck extends AbstractCheck { 062 063 /** 064 * A key is pointing to the warning message text in "messages.properties" 065 * file. 066 */ 067 public static final String MSG_FALL_THROUGH = "fall.through"; 068 069 /** 070 * A key is pointing to the warning message text in "messages.properties" 071 * file. 072 */ 073 public static final String MSG_FALL_THROUGH_LAST = "fall.through.last"; 074 075 /** Control whether the last case group must be checked. */ 076 private boolean checkLastCaseGroup; 077 078 /** 079 * Define the RegExp to match the relief comment that suppresses 080 * the warning about a fall through. 081 */ 082 private Pattern reliefPattern = Pattern.compile("falls?[ -]?thr(u|ough)"); 083 084 @Override 085 public int[] getDefaultTokens() { 086 return getRequiredTokens(); 087 } 088 089 @Override 090 public int[] getRequiredTokens() { 091 return new int[] {TokenTypes.CASE_GROUP}; 092 } 093 094 @Override 095 public int[] getAcceptableTokens() { 096 return getRequiredTokens(); 097 } 098 099 @Override 100 public boolean isCommentNodesRequired() { 101 return true; 102 } 103 104 /** 105 * Setter to define the RegExp to match the relief comment that suppresses 106 * the warning about a fall through. 107 * 108 * @param pattern 109 * The regular expression pattern. 110 * @since 4.0 111 */ 112 public void setReliefPattern(Pattern pattern) { 113 reliefPattern = pattern; 114 } 115 116 /** 117 * Setter to control whether the last case group must be checked. 118 * 119 * @param value new value of the property. 120 * @since 4.0 121 */ 122 public void setCheckLastCaseGroup(boolean value) { 123 checkLastCaseGroup = value; 124 } 125 126 @Override 127 public void visitToken(DetailAST ast) { 128 final DetailAST nextGroup = ast.getNextSibling(); 129 final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP; 130 if (!isLastGroup || checkLastCaseGroup) { 131 final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST); 132 133 if (slist != null && !isTerminated(slist, true, true, new HashSet<>()) 134 && !hasFallThroughComment(ast)) { 135 if (isLastGroup) { 136 log(ast, MSG_FALL_THROUGH_LAST); 137 } 138 else { 139 log(nextGroup, MSG_FALL_THROUGH); 140 } 141 } 142 } 143 } 144 145 /** 146 * Checks if a given subtree terminated by return, throw or, 147 * if allowed break, continue. 148 * When analyzing fall-through cases in switch statements, a Set of String labels 149 * is used to keep track of the labels encountered in the enclosing switch statements. 150 * 151 * @param ast root of given subtree 152 * @param useBreak should we consider break as terminator 153 * @param useContinue should we consider continue as terminator 154 * @param labelsForCurrentSwitchScope the Set labels for the current scope of the switch 155 * @return true if the subtree is terminated. 156 */ 157 private boolean isTerminated(final DetailAST ast, boolean useBreak, 158 boolean useContinue, Set<String> labelsForCurrentSwitchScope) { 159 160 return switch (ast.getType()) { 161 case TokenTypes.LITERAL_RETURN, TokenTypes.LITERAL_YIELD, 162 TokenTypes.LITERAL_THROW -> true; 163 case TokenTypes.LITERAL_BREAK -> useBreak 164 || hasLabel(ast, labelsForCurrentSwitchScope); 165 case TokenTypes.LITERAL_CONTINUE -> useContinue 166 || hasLabel(ast, labelsForCurrentSwitchScope); 167 case TokenTypes.SLIST -> checkSlist(ast, useBreak, useContinue, 168 labelsForCurrentSwitchScope); 169 case TokenTypes.LITERAL_IF -> checkIf(ast, useBreak, useContinue, 170 labelsForCurrentSwitchScope); 171 case TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_DO -> 172 checkLoop(ast, labelsForCurrentSwitchScope); 173 case TokenTypes.LITERAL_TRY -> checkTry(ast, useBreak, useContinue, 174 labelsForCurrentSwitchScope); 175 case TokenTypes.LITERAL_SWITCH -> checkSwitch(ast, useContinue, 176 labelsForCurrentSwitchScope); 177 case TokenTypes.LITERAL_SYNCHRONIZED -> 178 checkSynchronized(ast, useBreak, useContinue, 179 labelsForCurrentSwitchScope); 180 case TokenTypes.LABELED_STAT -> { 181 labelsForCurrentSwitchScope.add(ast.getFirstChild().getText()); 182 yield isTerminated(ast.getLastChild(), useBreak, useContinue, 183 labelsForCurrentSwitchScope); 184 } 185 default -> false; 186 }; 187 } 188 189 /** 190 * Checks if given break or continue ast has outer label. 191 * 192 * @param statement break or continue node 193 * @param labelsForCurrentSwitchScope the Set labels for the current scope of the switch 194 * @return true if local label used 195 */ 196 private static boolean hasLabel(DetailAST statement, Set<String> labelsForCurrentSwitchScope) { 197 return Optional.ofNullable(statement) 198 .map(DetailAST::getFirstChild) 199 .filter(child -> child.getType() == TokenTypes.IDENT) 200 .map(DetailAST::getText) 201 .filter(label -> !labelsForCurrentSwitchScope.contains(label)) 202 .isPresent(); 203 } 204 205 /** 206 * Checks if a given SLIST terminated by return, throw or, 207 * if allowed break, continue. 208 * 209 * @param slistAst SLIST to check 210 * @param useBreak should we consider break as terminator 211 * @param useContinue should we consider continue as terminator 212 * @param labels label names 213 * @return true if SLIST is terminated. 214 */ 215 private boolean checkSlist(final DetailAST slistAst, boolean useBreak, 216 boolean useContinue, Set<String> labels) { 217 DetailAST lastStmt = slistAst.getLastChild(); 218 219 if (lastStmt.getType() == TokenTypes.RCURLY) { 220 lastStmt = lastStmt.getPreviousSibling(); 221 } 222 223 while (TokenUtil.isOfType(lastStmt, TokenTypes.SINGLE_LINE_COMMENT, 224 TokenTypes.BLOCK_COMMENT_BEGIN)) { 225 lastStmt = lastStmt.getPreviousSibling(); 226 } 227 228 return lastStmt != null 229 && isTerminated(lastStmt, useBreak, useContinue, labels); 230 } 231 232 /** 233 * Checks if a given IF terminated by return, throw or, 234 * if allowed break, continue. 235 * 236 * @param ast IF to check 237 * @param useBreak should we consider break as terminator 238 * @param useContinue should we consider continue as terminator 239 * @param labels label names 240 * @return true if IF is terminated. 241 */ 242 private boolean checkIf(final DetailAST ast, boolean useBreak, 243 boolean useContinue, Set<String> labels) { 244 final DetailAST thenStmt = getNextNonCommentAst(ast.findFirstToken(TokenTypes.RPAREN)); 245 246 final DetailAST elseStmt = getNextNonCommentAst(thenStmt); 247 248 return elseStmt != null 249 && isTerminated(thenStmt, useBreak, useContinue, labels) 250 && isTerminated(elseStmt.getLastChild(), useBreak, useContinue, labels); 251 } 252 253 /** 254 * This method will skip the comment content while finding the next ast of current ast. 255 * 256 * @param ast current ast 257 * @return next ast after skipping comment 258 */ 259 private static DetailAST getNextNonCommentAst(DetailAST ast) { 260 DetailAST nextSibling = ast.getNextSibling(); 261 while (TokenUtil.isOfType(nextSibling, TokenTypes.SINGLE_LINE_COMMENT, 262 TokenTypes.BLOCK_COMMENT_BEGIN)) { 263 nextSibling = nextSibling.getNextSibling(); 264 } 265 return nextSibling; 266 } 267 268 /** 269 * Checks if a given loop terminated by return, throw or, 270 * if allowed break, continue. 271 * 272 * @param ast loop to check 273 * @param labels label names 274 * @return true if loop is terminated. 275 */ 276 private boolean checkLoop(final DetailAST ast, Set<String> labels) { 277 final DetailAST loopBody; 278 if (ast.getType() == TokenTypes.LITERAL_DO) { 279 final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE); 280 loopBody = lparen.getPreviousSibling(); 281 } 282 else { 283 final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN); 284 loopBody = rparen.getNextSibling(); 285 } 286 return isTerminated(loopBody, false, false, labels); 287 } 288 289 /** 290 * Checks if a given try/catch/finally block terminated by return, throw or, 291 * if allowed break, continue. 292 * 293 * @param ast loop to check 294 * @param useBreak should we consider break as terminator 295 * @param useContinue should we consider continue as terminator 296 * @param labels label names 297 * @return true if try/catch/finally block is terminated 298 */ 299 private boolean checkTry(final DetailAST ast, boolean useBreak, 300 boolean useContinue, Set<String> labels) { 301 final DetailAST finalStmt = ast.getLastChild(); 302 boolean isTerminated = finalStmt.getType() == TokenTypes.LITERAL_FINALLY 303 && isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST), 304 useBreak, useContinue, labels); 305 306 if (!isTerminated) { 307 DetailAST firstChild = ast.getFirstChild(); 308 309 if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) { 310 firstChild = firstChild.getNextSibling(); 311 } 312 313 isTerminated = isTerminated(firstChild, 314 useBreak, useContinue, labels); 315 316 DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH); 317 while (catchStmt != null 318 && isTerminated 319 && catchStmt.getType() == TokenTypes.LITERAL_CATCH) { 320 final DetailAST catchBody = 321 catchStmt.findFirstToken(TokenTypes.SLIST); 322 isTerminated = isTerminated(catchBody, useBreak, useContinue, labels); 323 catchStmt = catchStmt.getNextSibling(); 324 } 325 } 326 return isTerminated; 327 } 328 329 /** 330 * Checks if a given switch terminated by return, throw or, 331 * if allowed break, continue. 332 * 333 * @param literalSwitchAst loop to check 334 * @param useContinue should we consider continue as terminator 335 * @param labels label names 336 * @return true if switch is terminated 337 */ 338 private boolean checkSwitch(DetailAST literalSwitchAst, 339 boolean useContinue, Set<String> labels) { 340 DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP); 341 boolean isTerminated = caseGroup != null; 342 while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) { 343 final DetailAST caseBody = 344 caseGroup.findFirstToken(TokenTypes.SLIST); 345 isTerminated = caseBody != null 346 && isTerminated(caseBody, false, useContinue, labels); 347 caseGroup = caseGroup.getNextSibling(); 348 } 349 return isTerminated; 350 } 351 352 /** 353 * Checks if a given synchronized block terminated by return, throw or, 354 * if allowed break, continue. 355 * 356 * @param synchronizedAst synchronized block to check. 357 * @param useBreak should we consider break as terminator 358 * @param useContinue should we consider continue as terminator 359 * @param labels label names 360 * @return true if synchronized block is terminated 361 */ 362 private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak, 363 boolean useContinue, Set<String> labels) { 364 return isTerminated( 365 synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue, labels); 366 } 367 368 /** 369 * Determines if the fall through case between {@code currentCase} and 370 * {@code nextCase} is relieved by an appropriate comment. 371 * 372 * <p>Handles</p> 373 * <pre> 374 * case 1: 375 * /* FALLTHRU */ case 2: 376 * 377 * switch(i) { 378 * default: 379 * /* FALLTHRU */} 380 * 381 * case 1: 382 * // FALLTHRU 383 * case 2: 384 * 385 * switch(i) { 386 * default: 387 * // FALLTHRU 388 * </pre> 389 * 390 * @param currentCase AST of the case that falls through to the next case. 391 * @return True if a relief comment was found 392 */ 393 private boolean hasFallThroughComment(DetailAST currentCase) { 394 final DetailAST nextSibling = currentCase.getNextSibling(); 395 final DetailAST ast; 396 if (nextSibling.getType() == TokenTypes.CASE_GROUP) { 397 ast = nextSibling.getFirstChild(); 398 } 399 else { 400 ast = currentCase; 401 } 402 return hasReliefComment(ast); 403 } 404 405 /** 406 * Check if there is any fall through comment. 407 * 408 * @param ast ast to check 409 * @return true if relief comment found 410 */ 411 private boolean hasReliefComment(DetailAST ast) { 412 final DetailAST nonCommentAst = getNextNonCommentAst(ast); 413 boolean result = false; 414 if (nonCommentAst != null) { 415 final int prevLineNumber = nonCommentAst.getPreviousSibling().getLineNo(); 416 result = Stream.iterate(nonCommentAst.getPreviousSibling(), 417 Objects::nonNull, 418 DetailAST::getPreviousSibling) 419 .takeWhile(sibling -> sibling.getLineNo() == prevLineNumber) 420 .map(DetailAST::getFirstChild) 421 .filter(Objects::nonNull) 422 .anyMatch(firstChild -> reliefPattern.matcher(firstChild.getText()).find()); 423 } 424 return result; 425 } 426 427}