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