001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2022 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.regex.Matcher; 023import java.util.regex.Pattern; 024 025import com.puppycrawl.tools.checkstyle.StatelessCheck; 026import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.TokenTypes; 029import com.puppycrawl.tools.checkstyle.utils.CodePointUtil; 030 031/** 032 * <p> 033 * Checks for fall-through in {@code switch} statements. 034 * Finds locations where a {@code case} <b>contains</b> Java code but lacks a 035 * {@code break}, {@code return}, {@code yield}, {@code throw} or {@code continue} statement. 036 * </p> 037 * <p> 038 * The check honors special comments to suppress the warning. 039 * By default, the texts 040 * "fallthru", "fall thru", "fall-thru", 041 * "fallthrough", "fall through", "fall-through" 042 * "fallsthrough", "falls through", "falls-through" (case-sensitive). 043 * The comment containing these words must be all on one line, 044 * and must be on the last non-empty line before the {@code case} triggering 045 * the warning or on the same line before the {@code case}(ugly, but possible). 046 * </p> 047 * <p> 048 * Note: The check assumes that there is no unreachable code in the {@code case}. 049 * </p> 050 * <ul> 051 * <li> 052 * Property {@code checkLastCaseGroup} - Control whether the last case group must be checked. 053 * Type is {@code boolean}. 054 * Default value is {@code false}. 055 * </li> 056 * <li> 057 * Property {@code reliefPattern} - Define the RegExp to match the relief comment that suppresses 058 * the warning about a fall through. 059 * Type is {@code java.util.regex.Pattern}. 060 * Default value is {@code "falls?[ -]?thr(u|ough)"}. 061 * </li> 062 * </ul> 063 * <p> 064 * To configure the check: 065 * </p> 066 * <pre> 067 * <module name="FallThrough"/> 068 * </pre> 069 * <p> 070 * Example: 071 * </p> 072 * <pre> 073 * public void foo() throws Exception { 074 * int i = 0; 075 * while (i >= 0) { 076 * switch (i) { 077 * case 1: 078 * i++; 079 * case 2: // violation, previous case contains code but lacks 080 * // break, return, yield, throw or continue statement 081 * i++; 082 * break; 083 * case 3: // OK 084 * i++; 085 * return; 086 * case 4: // OK 087 * i++; 088 * throw new Exception(); 089 * case 5: // OK 090 * i++; 091 * continue; 092 * case 6: // OK 093 * case 7: // Previous case: OK, case does not contain code 094 * // This case: OK, by default the last case might not have statement 095 * // that transfer control 096 * i++; 097 * } 098 * } 099 * } 100 * public int bar() { 101 * int i = 0; 102 * return switch (i) { 103 * case 1: 104 * i++; 105 * case 2: // violation, previous case contains code but lacks 106 * // break, return, yield, throw or continue statement 107 * case 3: // OK, case does not contain code 108 * i++; 109 * yield 11; 110 * default: // OK 111 * yield -1; 112 * }; 113 * } 114 * </pre> 115 * <p> 116 * Example how to suppress violations by comment: 117 * </p> 118 * <pre> 119 * switch (i) { 120 * case 1: 121 * i++; // fall through 122 * 123 * case 2: // OK 124 * i++; 125 * // fallthru 126 * case 3: { // OK 127 * i++; 128 * } 129 * /* fall-thru */ 130 * case 4: // OK 131 * i++; 132 * // Fallthru 133 * case 5: // violation, "Fallthru" in case 4 should be "fallthru" 134 * i++; 135 * // fall through 136 * i++; 137 * case 6: // violation, the comment must be on the last non-empty line before 'case' 138 * i++; 139 * /* fall through */case 7: // OK, comment can appear on the same line but before 'case' 140 * i++; 141 * } 142 * </pre> 143 * <p> 144 * To configure the check to enable check for last case group: 145 * </p> 146 * <pre> 147 * <module name="FallThrough"> 148 * <property name="checkLastCaseGroup" value="true"/> 149 * </module> 150 * </pre> 151 * <p> 152 * Example: 153 * </p> 154 * <pre> 155 * switch (i) { 156 * case 1: 157 * break; 158 * case 2: // Previous case: OK 159 * // This case: violation, last case must have statement that transfer control 160 * i++; 161 * } 162 * </pre> 163 * <p> 164 * To configure the check with custom relief pattern: 165 * </p> 166 * <pre> 167 * <module name="FallThrough"> 168 * <property name="reliefPattern" value="FALL?[ -]?THROUGH"/> 169 * </module> 170 * </pre> 171 * <p> 172 * Example: 173 * </p> 174 * <pre> 175 * switch (i) { 176 * case 1: 177 * i++; 178 * // FALL-THROUGH 179 * case 2: // OK, "FALL-THROUGH" matches the regular expression "FALL?[ -]?THROUGH" 180 * i++; 181 * // fall-through 182 * case 3: // violation, "fall-through" doesn't match 183 * break; 184 * } 185 * </pre> 186 * <p> 187 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 188 * </p> 189 * <p> 190 * Violation Message Keys: 191 * </p> 192 * <ul> 193 * <li> 194 * {@code fall.through} 195 * </li> 196 * <li> 197 * {@code fall.through.last} 198 * </li> 199 * </ul> 200 * 201 * @since 3.4 202 */ 203@StatelessCheck 204public class FallThroughCheck extends AbstractCheck { 205 206 /** 207 * A key is pointing to the warning message text in "messages.properties" 208 * file. 209 */ 210 public static final String MSG_FALL_THROUGH = "fall.through"; 211 212 /** 213 * A key is pointing to the warning message text in "messages.properties" 214 * file. 215 */ 216 public static final String MSG_FALL_THROUGH_LAST = "fall.through.last"; 217 218 /** Control whether the last case group must be checked. */ 219 private boolean checkLastCaseGroup; 220 221 /** 222 * Define the RegExp to match the relief comment that suppresses 223 * the warning about a fall through. 224 */ 225 private Pattern reliefPattern = Pattern.compile("falls?[ -]?thr(u|ough)"); 226 227 @Override 228 public int[] getDefaultTokens() { 229 return getRequiredTokens(); 230 } 231 232 @Override 233 public int[] getRequiredTokens() { 234 return new int[] {TokenTypes.CASE_GROUP}; 235 } 236 237 @Override 238 public int[] getAcceptableTokens() { 239 return getRequiredTokens(); 240 } 241 242 /** 243 * Setter to define the RegExp to match the relief comment that suppresses 244 * the warning about a fall through. 245 * 246 * @param pattern 247 * The regular expression pattern. 248 */ 249 public void setReliefPattern(Pattern pattern) { 250 reliefPattern = pattern; 251 } 252 253 /** 254 * Setter to control whether the last case group must be checked. 255 * 256 * @param value new value of the property. 257 */ 258 public void setCheckLastCaseGroup(boolean value) { 259 checkLastCaseGroup = value; 260 } 261 262 @Override 263 public void visitToken(DetailAST ast) { 264 final DetailAST nextGroup = ast.getNextSibling(); 265 final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP; 266 if (!isLastGroup || checkLastCaseGroup) { 267 final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST); 268 269 if (slist != null && !isTerminated(slist, true, true) 270 && !hasFallThroughComment(ast, nextGroup)) { 271 if (isLastGroup) { 272 log(ast, MSG_FALL_THROUGH_LAST); 273 } 274 else { 275 log(nextGroup, MSG_FALL_THROUGH); 276 } 277 } 278 } 279 } 280 281 /** 282 * Checks if a given subtree terminated by return, throw or, 283 * if allowed break, continue. 284 * 285 * @param ast root of given subtree 286 * @param useBreak should we consider break as terminator 287 * @param useContinue should we consider continue as terminator 288 * @return true if the subtree is terminated. 289 */ 290 private boolean isTerminated(final DetailAST ast, boolean useBreak, 291 boolean useContinue) { 292 final boolean terminated; 293 294 switch (ast.getType()) { 295 case TokenTypes.LITERAL_RETURN: 296 case TokenTypes.LITERAL_YIELD: 297 case TokenTypes.LITERAL_THROW: 298 terminated = true; 299 break; 300 case TokenTypes.LITERAL_BREAK: 301 terminated = useBreak; 302 break; 303 case TokenTypes.LITERAL_CONTINUE: 304 terminated = useContinue; 305 break; 306 case TokenTypes.SLIST: 307 terminated = checkSlist(ast, useBreak, useContinue); 308 break; 309 case TokenTypes.LITERAL_IF: 310 terminated = checkIf(ast, useBreak, useContinue); 311 break; 312 case TokenTypes.LITERAL_FOR: 313 case TokenTypes.LITERAL_WHILE: 314 case TokenTypes.LITERAL_DO: 315 terminated = checkLoop(ast); 316 break; 317 case TokenTypes.LITERAL_TRY: 318 terminated = checkTry(ast, useBreak, useContinue); 319 break; 320 case TokenTypes.LITERAL_SWITCH: 321 terminated = checkSwitch(ast, useContinue); 322 break; 323 case TokenTypes.LITERAL_SYNCHRONIZED: 324 terminated = checkSynchronized(ast, useBreak, useContinue); 325 break; 326 default: 327 terminated = false; 328 } 329 return terminated; 330 } 331 332 /** 333 * Checks if a given SLIST terminated by return, throw or, 334 * if allowed break, continue. 335 * 336 * @param slistAst SLIST to check 337 * @param useBreak should we consider break as terminator 338 * @param useContinue should we consider continue as terminator 339 * @return true if SLIST is terminated. 340 */ 341 private boolean checkSlist(final DetailAST slistAst, boolean useBreak, 342 boolean useContinue) { 343 DetailAST lastStmt = slistAst.getLastChild(); 344 345 if (lastStmt.getType() == TokenTypes.RCURLY) { 346 lastStmt = lastStmt.getPreviousSibling(); 347 } 348 349 return lastStmt != null 350 && isTerminated(lastStmt, useBreak, useContinue); 351 } 352 353 /** 354 * Checks if a given IF terminated by return, throw or, 355 * if allowed break, continue. 356 * 357 * @param ast IF to check 358 * @param useBreak should we consider break as terminator 359 * @param useContinue should we consider continue as terminator 360 * @return true if IF is terminated. 361 */ 362 private boolean checkIf(final DetailAST ast, boolean useBreak, 363 boolean useContinue) { 364 final DetailAST thenStmt = ast.findFirstToken(TokenTypes.RPAREN) 365 .getNextSibling(); 366 final DetailAST elseStmt = thenStmt.getNextSibling(); 367 368 return elseStmt != null 369 && isTerminated(thenStmt, useBreak, useContinue) 370 && isTerminated(elseStmt.getFirstChild(), useBreak, useContinue); 371 } 372 373 /** 374 * Checks if a given loop terminated by return, throw or, 375 * if allowed break, continue. 376 * 377 * @param ast loop to check 378 * @return true if loop is terminated. 379 */ 380 private boolean checkLoop(final DetailAST ast) { 381 final DetailAST loopBody; 382 if (ast.getType() == TokenTypes.LITERAL_DO) { 383 final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE); 384 loopBody = lparen.getPreviousSibling(); 385 } 386 else { 387 final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN); 388 loopBody = rparen.getNextSibling(); 389 } 390 return isTerminated(loopBody, false, false); 391 } 392 393 /** 394 * Checks if a given try/catch/finally block terminated by return, throw or, 395 * if allowed break, continue. 396 * 397 * @param ast loop to check 398 * @param useBreak should we consider break as terminator 399 * @param useContinue should we consider continue as terminator 400 * @return true if try/catch/finally block is terminated 401 */ 402 private boolean checkTry(final DetailAST ast, boolean useBreak, 403 boolean useContinue) { 404 final DetailAST finalStmt = ast.getLastChild(); 405 boolean isTerminated = finalStmt.getType() == TokenTypes.LITERAL_FINALLY 406 && isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST), useBreak, useContinue); 407 408 if (!isTerminated) { 409 DetailAST firstChild = ast.getFirstChild(); 410 411 if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) { 412 firstChild = firstChild.getNextSibling(); 413 } 414 415 isTerminated = isTerminated(firstChild, 416 useBreak, useContinue); 417 418 DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH); 419 while (catchStmt != null 420 && isTerminated 421 && catchStmt.getType() == TokenTypes.LITERAL_CATCH) { 422 final DetailAST catchBody = 423 catchStmt.findFirstToken(TokenTypes.SLIST); 424 isTerminated = isTerminated(catchBody, useBreak, useContinue); 425 catchStmt = catchStmt.getNextSibling(); 426 } 427 } 428 return isTerminated; 429 } 430 431 /** 432 * Checks if a given switch terminated by return, throw or, 433 * if allowed break, continue. 434 * 435 * @param literalSwitchAst loop to check 436 * @param useContinue should we consider continue as terminator 437 * @return true if switch is terminated 438 */ 439 private boolean checkSwitch(final DetailAST literalSwitchAst, boolean useContinue) { 440 DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP); 441 boolean isTerminated = caseGroup != null; 442 while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) { 443 final DetailAST caseBody = 444 caseGroup.findFirstToken(TokenTypes.SLIST); 445 isTerminated = caseBody != null && isTerminated(caseBody, false, useContinue); 446 caseGroup = caseGroup.getNextSibling(); 447 } 448 return isTerminated; 449 } 450 451 /** 452 * Checks if a given synchronized block terminated by return, throw or, 453 * if allowed break, continue. 454 * 455 * @param synchronizedAst synchronized block to check. 456 * @param useBreak should we consider break as terminator 457 * @param useContinue should we consider continue as terminator 458 * @return true if synchronized block is terminated 459 */ 460 private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak, 461 boolean useContinue) { 462 return isTerminated( 463 synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue); 464 } 465 466 /** 467 * Determines if the fall through case between {@code currentCase} and 468 * {@code nextCase} is relieved by an appropriate comment. 469 * 470 * <p>Handles</p> 471 * <pre> 472 * case 1: 473 * /* FALLTHRU */ case 2: 474 * 475 * switch(i) { 476 * default: 477 * /* FALLTHRU */} 478 * 479 * case 1: 480 * // FALLTHRU 481 * case 2: 482 * 483 * switch(i) { 484 * default: 485 * // FALLTHRU 486 * </pre> 487 * 488 * @param currentCase AST of the case that falls through to the next case. 489 * @param nextCase AST of the next case. 490 * @return True if a relief comment was found 491 */ 492 private boolean hasFallThroughComment(DetailAST currentCase, DetailAST nextCase) { 493 boolean allThroughComment = false; 494 final int endLineNo = nextCase.getLineNo(); 495 496 if (matchesComment(reliefPattern, endLineNo)) { 497 allThroughComment = true; 498 } 499 else { 500 final int startLineNo = currentCase.getLineNo(); 501 for (int i = endLineNo - 2; i > startLineNo - 1; i--) { 502 final int[] line = getLineCodePoints(i); 503 if (!CodePointUtil.isBlank(line)) { 504 allThroughComment = matchesComment(reliefPattern, i + 1); 505 break; 506 } 507 } 508 } 509 return allThroughComment; 510 } 511 512 /** 513 * Does a regular expression match on the given line and checks that a 514 * possible match is within a comment. 515 * 516 * @param pattern The regular expression pattern to use. 517 * @param lineNo The line number in the file. 518 * @return True if a match was found inside a comment. 519 */ 520 // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166 521 @SuppressWarnings("deprecation") 522 private boolean matchesComment(Pattern pattern, int lineNo) { 523 final String line = getLine(lineNo - 1); 524 525 final Matcher matcher = pattern.matcher(line); 526 return matcher.find() 527 && getFileContents().hasIntersectionWithComment( 528 lineNo, matcher.start(), lineNo, matcher.end()); 529 } 530 531}