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.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 final boolean terminated; 189 190 switch (ast.getType()) { 191 case TokenTypes.LITERAL_RETURN: 192 case TokenTypes.LITERAL_YIELD: 193 case TokenTypes.LITERAL_THROW: 194 terminated = true; 195 break; 196 case TokenTypes.LITERAL_BREAK: 197 terminated = 198 useBreak || hasLabel(ast, labelsForCurrentSwitchScope); 199 break; 200 case TokenTypes.LITERAL_CONTINUE: 201 terminated = 202 useContinue || hasLabel(ast, labelsForCurrentSwitchScope); 203 break; 204 case TokenTypes.SLIST: 205 terminated = 206 checkSlist(ast, useBreak, useContinue, labelsForCurrentSwitchScope); 207 break; 208 case TokenTypes.LITERAL_IF: 209 terminated = 210 checkIf(ast, useBreak, useContinue, labelsForCurrentSwitchScope); 211 break; 212 case TokenTypes.LITERAL_FOR: 213 case TokenTypes.LITERAL_WHILE: 214 case TokenTypes.LITERAL_DO: 215 terminated = checkLoop(ast, labelsForCurrentSwitchScope); 216 break; 217 case TokenTypes.LITERAL_TRY: 218 terminated = 219 checkTry(ast, useBreak, useContinue, labelsForCurrentSwitchScope); 220 break; 221 case TokenTypes.LITERAL_SWITCH: 222 terminated = 223 checkSwitch(ast, useContinue, labelsForCurrentSwitchScope); 224 break; 225 case TokenTypes.LITERAL_SYNCHRONIZED: 226 terminated = 227 checkSynchronized(ast, useBreak, useContinue, labelsForCurrentSwitchScope); 228 break; 229 case TokenTypes.LABELED_STAT: 230 labelsForCurrentSwitchScope.add(ast.getFirstChild().getText()); 231 terminated = 232 isTerminated(ast.getLastChild(), useBreak, useContinue, 233 labelsForCurrentSwitchScope); 234 break; 235 default: 236 terminated = false; 237 } 238 return terminated; 239 } 240 241 /** 242 * Checks if given break or continue ast has outer label. 243 * 244 * @param statement break or continue node 245 * @param labelsForCurrentSwitchScope the Set labels for the current scope of the switch 246 * @return true if local label used 247 */ 248 private static boolean hasLabel(DetailAST statement, Set<String> labelsForCurrentSwitchScope) { 249 return Optional.ofNullable(statement) 250 .map(DetailAST::getFirstChild) 251 .filter(child -> child.getType() == TokenTypes.IDENT) 252 .map(DetailAST::getText) 253 .filter(label -> !labelsForCurrentSwitchScope.contains(label)) 254 .isPresent(); 255 } 256 257 /** 258 * Checks if a given SLIST terminated by return, throw or, 259 * if allowed break, continue. 260 * 261 * @param slistAst SLIST to check 262 * @param useBreak should we consider break as terminator 263 * @param useContinue should we consider continue as terminator 264 * @param labels label names 265 * @return true if SLIST is terminated. 266 */ 267 private boolean checkSlist(final DetailAST slistAst, boolean useBreak, 268 boolean useContinue, Set<String> labels) { 269 DetailAST lastStmt = slistAst.getLastChild(); 270 271 if (lastStmt.getType() == TokenTypes.RCURLY) { 272 lastStmt = lastStmt.getPreviousSibling(); 273 } 274 275 while (TokenUtil.isOfType(lastStmt, TokenTypes.SINGLE_LINE_COMMENT, 276 TokenTypes.BLOCK_COMMENT_BEGIN)) { 277 lastStmt = lastStmt.getPreviousSibling(); 278 } 279 280 return lastStmt != null 281 && isTerminated(lastStmt, useBreak, useContinue, labels); 282 } 283 284 /** 285 * Checks if a given IF terminated by return, throw or, 286 * if allowed break, continue. 287 * 288 * @param ast IF to check 289 * @param useBreak should we consider break as terminator 290 * @param useContinue should we consider continue as terminator 291 * @param labels label names 292 * @return true if IF is terminated. 293 */ 294 private boolean checkIf(final DetailAST ast, boolean useBreak, 295 boolean useContinue, Set<String> labels) { 296 final DetailAST thenStmt = getNextNonCommentAst(ast.findFirstToken(TokenTypes.RPAREN)); 297 298 final DetailAST elseStmt = getNextNonCommentAst(thenStmt); 299 300 return elseStmt != null 301 && isTerminated(thenStmt, useBreak, useContinue, labels) 302 && isTerminated(elseStmt.getLastChild(), useBreak, useContinue, labels); 303 } 304 305 /** 306 * This method will skip the comment content while finding the next ast of current ast. 307 * 308 * @param ast current ast 309 * @return next ast after skipping comment 310 */ 311 private static DetailAST getNextNonCommentAst(DetailAST ast) { 312 DetailAST nextSibling = ast.getNextSibling(); 313 while (TokenUtil.isOfType(nextSibling, TokenTypes.SINGLE_LINE_COMMENT, 314 TokenTypes.BLOCK_COMMENT_BEGIN)) { 315 nextSibling = nextSibling.getNextSibling(); 316 } 317 return nextSibling; 318 } 319 320 /** 321 * Checks if a given loop terminated by return, throw or, 322 * if allowed break, continue. 323 * 324 * @param ast loop to check 325 * @param labels label names 326 * @return true if loop is terminated. 327 */ 328 private boolean checkLoop(final DetailAST ast, Set<String> labels) { 329 final DetailAST loopBody; 330 if (ast.getType() == TokenTypes.LITERAL_DO) { 331 final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE); 332 loopBody = lparen.getPreviousSibling(); 333 } 334 else { 335 final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN); 336 loopBody = rparen.getNextSibling(); 337 } 338 return isTerminated(loopBody, false, false, labels); 339 } 340 341 /** 342 * Checks if a given try/catch/finally block terminated by return, throw or, 343 * if allowed break, continue. 344 * 345 * @param ast loop to check 346 * @param useBreak should we consider break as terminator 347 * @param useContinue should we consider continue as terminator 348 * @param labels label names 349 * @return true if try/catch/finally block is terminated 350 */ 351 private boolean checkTry(final DetailAST ast, boolean useBreak, 352 boolean useContinue, Set<String> labels) { 353 final DetailAST finalStmt = ast.getLastChild(); 354 boolean isTerminated = finalStmt.getType() == TokenTypes.LITERAL_FINALLY 355 && isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST), 356 useBreak, useContinue, labels); 357 358 if (!isTerminated) { 359 DetailAST firstChild = ast.getFirstChild(); 360 361 if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) { 362 firstChild = firstChild.getNextSibling(); 363 } 364 365 isTerminated = isTerminated(firstChild, 366 useBreak, useContinue, labels); 367 368 DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH); 369 while (catchStmt != null 370 && isTerminated 371 && catchStmt.getType() == TokenTypes.LITERAL_CATCH) { 372 final DetailAST catchBody = 373 catchStmt.findFirstToken(TokenTypes.SLIST); 374 isTerminated = isTerminated(catchBody, useBreak, useContinue, labels); 375 catchStmt = catchStmt.getNextSibling(); 376 } 377 } 378 return isTerminated; 379 } 380 381 /** 382 * Checks if a given switch terminated by return, throw or, 383 * if allowed break, continue. 384 * 385 * @param literalSwitchAst loop to check 386 * @param useContinue should we consider continue as terminator 387 * @param labels label names 388 * @return true if switch is terminated 389 */ 390 private boolean checkSwitch(DetailAST literalSwitchAst, 391 boolean useContinue, Set<String> labels) { 392 DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP); 393 boolean isTerminated = caseGroup != null; 394 while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) { 395 final DetailAST caseBody = 396 caseGroup.findFirstToken(TokenTypes.SLIST); 397 isTerminated = caseBody != null 398 && isTerminated(caseBody, false, useContinue, labels); 399 caseGroup = caseGroup.getNextSibling(); 400 } 401 return isTerminated; 402 } 403 404 /** 405 * Checks if a given synchronized block terminated by return, throw or, 406 * if allowed break, continue. 407 * 408 * @param synchronizedAst synchronized block to check. 409 * @param useBreak should we consider break as terminator 410 * @param useContinue should we consider continue as terminator 411 * @param labels label names 412 * @return true if synchronized block is terminated 413 */ 414 private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak, 415 boolean useContinue, Set<String> labels) { 416 return isTerminated( 417 synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue, labels); 418 } 419 420 /** 421 * Determines if the fall through case between {@code currentCase} and 422 * {@code nextCase} is relieved by an appropriate comment. 423 * 424 * <p>Handles</p> 425 * <pre> 426 * case 1: 427 * /* FALLTHRU */ case 2: 428 * 429 * switch(i) { 430 * default: 431 * /* FALLTHRU */} 432 * 433 * case 1: 434 * // FALLTHRU 435 * case 2: 436 * 437 * switch(i) { 438 * default: 439 * // FALLTHRU 440 * </pre> 441 * 442 * @param currentCase AST of the case that falls through to the next case. 443 * @return True if a relief comment was found 444 */ 445 private boolean hasFallThroughComment(DetailAST currentCase) { 446 final DetailAST nextSibling = currentCase.getNextSibling(); 447 final DetailAST ast; 448 if (nextSibling.getType() == TokenTypes.CASE_GROUP) { 449 ast = nextSibling.getFirstChild(); 450 } 451 else { 452 ast = currentCase; 453 } 454 return hasReliefComment(ast); 455 } 456 457 /** 458 * Check if there is any fall through comment. 459 * 460 * @param ast ast to check 461 * @return true if relief comment found 462 */ 463 private boolean hasReliefComment(DetailAST ast) { 464 final DetailAST nonCommentAst = getNextNonCommentAst(ast); 465 boolean result = false; 466 if (nonCommentAst != null) { 467 final int prevLineNumber = nonCommentAst.getPreviousSibling().getLineNo(); 468 result = Stream.iterate(nonCommentAst.getPreviousSibling(), 469 Objects::nonNull, 470 DetailAST::getPreviousSibling) 471 .takeWhile(sibling -> sibling.getLineNo() == prevLineNumber) 472 .map(DetailAST::getFirstChild) 473 .filter(Objects::nonNull) 474 .anyMatch(firstChild -> reliefPattern.matcher(firstChild.getText()).find()); 475 } 476 return result; 477 } 478 479}