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