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