001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2020 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.CommonUtil; 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 * <pre> 048 * switch (i) { 049 * case 0: 050 * i++; // fall through 051 * 052 * case 1: 053 * i++; 054 * // falls through 055 * case 2: 056 * case 3: 057 * case 4: { 058 * i++; 059 * } 060 * // fallthrough 061 * case 5: 062 * i++; 063 * /* fallthru */case 6: 064 * i++; 065 * // fall-through 066 * case 7: 067 * i++; 068 * break; 069 * } 070 * </pre> 071 * <p> 072 * Note: The check assumes that there is no unreachable code in the {@code case}. 073 * </p> 074 * <p> 075 * The following fragment of code will NOT trigger the check, 076 * because of the comment "fallthru" or any Java code 077 * in case 5 are absent. 078 * </p> 079 * <pre> 080 * case 3: 081 * x = 2; 082 * // fallthru 083 * case 4: 084 * case 5: // violation 085 * case 6: 086 * break; 087 * </pre> 088 * <ul> 089 * <li> 090 * Property {@code checkLastCaseGroup} - Control whether the last case group must be checked. 091 * Type is {@code boolean}. 092 * Default value is {@code false}. 093 * </li> 094 * <li> 095 * Property {@code reliefPattern} - Define the RegExp to match the relief comment that suppresses 096 * the warning about a fall through. 097 * Type is {@code java.util.regex.Pattern}. 098 * Default value is {@code "falls?[ -]?thr(u|ough)"}. 099 * </li> 100 * </ul> 101 * <p> 102 * To configure the check: 103 * </p> 104 * <pre> 105 * <module name="FallThrough"/> 106 * </pre> 107 * <p> 108 * or 109 * </p> 110 * <pre> 111 * <module name="FallThrough"> 112 * <property name="reliefPattern" value="continue in next case"/> 113 * </module> 114 * </pre> 115 * <p> 116 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 117 * </p> 118 * <p> 119 * Violation Message Keys: 120 * </p> 121 * <ul> 122 * <li> 123 * {@code fall.through} 124 * </li> 125 * <li> 126 * {@code fall.through.last} 127 * </li> 128 * </ul> 129 * 130 * @since 3.4 131 */ 132@StatelessCheck 133public class FallThroughCheck extends AbstractCheck { 134 135 /** 136 * A key is pointing to the warning message text in "messages.properties" 137 * file. 138 */ 139 public static final String MSG_FALL_THROUGH = "fall.through"; 140 141 /** 142 * A key is pointing to the warning message text in "messages.properties" 143 * file. 144 */ 145 public static final String MSG_FALL_THROUGH_LAST = "fall.through.last"; 146 147 /** Control whether the last case group must be checked. */ 148 private boolean checkLastCaseGroup; 149 150 /** 151 * Define the RegExp to match the relief comment that suppresses 152 * the warning about a fall through. 153 */ 154 private Pattern reliefPattern = Pattern.compile("falls?[ -]?thr(u|ough)"); 155 156 @Override 157 public int[] getDefaultTokens() { 158 return getRequiredTokens(); 159 } 160 161 @Override 162 public int[] getRequiredTokens() { 163 return new int[] {TokenTypes.CASE_GROUP}; 164 } 165 166 @Override 167 public int[] getAcceptableTokens() { 168 return getRequiredTokens(); 169 } 170 171 /** 172 * Setter to define the RegExp to match the relief comment that suppresses 173 * the warning about a fall through. 174 * 175 * @param pattern 176 * The regular expression pattern. 177 */ 178 public void setReliefPattern(Pattern pattern) { 179 reliefPattern = pattern; 180 } 181 182 /** 183 * Setter to control whether the last case group must be checked. 184 * 185 * @param value new value of the property. 186 */ 187 public void setCheckLastCaseGroup(boolean value) { 188 checkLastCaseGroup = value; 189 } 190 191 @Override 192 public void visitToken(DetailAST ast) { 193 final DetailAST nextGroup = ast.getNextSibling(); 194 final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP; 195 if (!isLastGroup || checkLastCaseGroup) { 196 final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST); 197 198 if (slist != null && !isTerminated(slist, true, true) 199 && !hasFallThroughComment(ast, nextGroup)) { 200 if (isLastGroup) { 201 log(ast, MSG_FALL_THROUGH_LAST); 202 } 203 else { 204 log(nextGroup, MSG_FALL_THROUGH); 205 } 206 } 207 } 208 } 209 210 /** 211 * Checks if a given subtree terminated by return, throw or, 212 * if allowed break, continue. 213 * 214 * @param ast root of given subtree 215 * @param useBreak should we consider break as terminator. 216 * @param useContinue should we consider continue as terminator. 217 * @return true if the subtree is terminated. 218 */ 219 private boolean isTerminated(final DetailAST ast, boolean useBreak, 220 boolean useContinue) { 221 final boolean terminated; 222 223 switch (ast.getType()) { 224 case TokenTypes.LITERAL_RETURN: 225 case TokenTypes.LITERAL_THROW: 226 terminated = true; 227 break; 228 case TokenTypes.LITERAL_BREAK: 229 terminated = useBreak; 230 break; 231 case TokenTypes.LITERAL_CONTINUE: 232 terminated = useContinue; 233 break; 234 case TokenTypes.SLIST: 235 terminated = checkSlist(ast, useBreak, useContinue); 236 break; 237 case TokenTypes.LITERAL_IF: 238 terminated = checkIf(ast, useBreak, useContinue); 239 break; 240 case TokenTypes.LITERAL_FOR: 241 case TokenTypes.LITERAL_WHILE: 242 case TokenTypes.LITERAL_DO: 243 terminated = checkLoop(ast); 244 break; 245 case TokenTypes.LITERAL_TRY: 246 terminated = checkTry(ast, useBreak, useContinue); 247 break; 248 case TokenTypes.LITERAL_SWITCH: 249 terminated = checkSwitch(ast, useContinue); 250 break; 251 case TokenTypes.LITERAL_SYNCHRONIZED: 252 terminated = checkSynchronized(ast, useBreak, useContinue); 253 break; 254 default: 255 terminated = false; 256 } 257 return terminated; 258 } 259 260 /** 261 * Checks if a given SLIST terminated by return, throw or, 262 * if allowed break, continue. 263 * 264 * @param slistAst SLIST to check 265 * @param useBreak should we consider break as terminator. 266 * @param useContinue should we consider continue as terminator. 267 * @return true if SLIST is terminated. 268 */ 269 private boolean checkSlist(final DetailAST slistAst, boolean useBreak, 270 boolean useContinue) { 271 DetailAST lastStmt = slistAst.getLastChild(); 272 273 if (lastStmt.getType() == TokenTypes.RCURLY) { 274 lastStmt = lastStmt.getPreviousSibling(); 275 } 276 277 return lastStmt != null 278 && isTerminated(lastStmt, useBreak, useContinue); 279 } 280 281 /** 282 * Checks if a given IF terminated by return, throw or, 283 * if allowed break, continue. 284 * 285 * @param ast IF to check 286 * @param useBreak should we consider break as terminator. 287 * @param useContinue should we consider continue as terminator. 288 * @return true if IF is terminated. 289 */ 290 private boolean checkIf(final DetailAST ast, boolean useBreak, 291 boolean useContinue) { 292 final DetailAST thenStmt = ast.findFirstToken(TokenTypes.RPAREN) 293 .getNextSibling(); 294 final DetailAST elseStmt = thenStmt.getNextSibling(); 295 296 return elseStmt != null 297 && isTerminated(thenStmt, useBreak, useContinue) 298 && isTerminated(elseStmt.getFirstChild(), useBreak, useContinue); 299 } 300 301 /** 302 * Checks if a given loop terminated by return, throw or, 303 * if allowed break, continue. 304 * 305 * @param ast loop to check 306 * @return true if loop is terminated. 307 */ 308 private boolean checkLoop(final DetailAST ast) { 309 final DetailAST loopBody; 310 if (ast.getType() == TokenTypes.LITERAL_DO) { 311 final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE); 312 loopBody = lparen.getPreviousSibling(); 313 } 314 else { 315 final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN); 316 loopBody = rparen.getNextSibling(); 317 } 318 return isTerminated(loopBody, false, false); 319 } 320 321 /** 322 * Checks if a given try/catch/finally block terminated by return, throw or, 323 * if allowed break, continue. 324 * 325 * @param ast loop to check 326 * @param useBreak should we consider break as terminator. 327 * @param useContinue should we consider continue as terminator. 328 * @return true if try/catch/finally block is terminated. 329 */ 330 private boolean checkTry(final DetailAST ast, boolean useBreak, 331 boolean useContinue) { 332 final DetailAST finalStmt = ast.getLastChild(); 333 boolean isTerminated = false; 334 if (finalStmt.getType() == TokenTypes.LITERAL_FINALLY) { 335 isTerminated = isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST), 336 useBreak, useContinue); 337 } 338 339 if (!isTerminated) { 340 DetailAST firstChild = ast.getFirstChild(); 341 342 if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) { 343 firstChild = firstChild.getNextSibling(); 344 } 345 346 isTerminated = isTerminated(firstChild, 347 useBreak, useContinue); 348 349 DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH); 350 while (catchStmt != null 351 && isTerminated 352 && catchStmt.getType() == TokenTypes.LITERAL_CATCH) { 353 final DetailAST catchBody = 354 catchStmt.findFirstToken(TokenTypes.SLIST); 355 isTerminated = isTerminated(catchBody, useBreak, useContinue); 356 catchStmt = catchStmt.getNextSibling(); 357 } 358 } 359 return isTerminated; 360 } 361 362 /** 363 * Checks if a given switch terminated by return, throw or, 364 * if allowed break, continue. 365 * 366 * @param literalSwitchAst loop to check 367 * @param useContinue should we consider continue as terminator. 368 * @return true if switch is terminated. 369 */ 370 private boolean checkSwitch(final DetailAST literalSwitchAst, boolean useContinue) { 371 DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP); 372 boolean isTerminated = caseGroup != null; 373 while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) { 374 final DetailAST caseBody = 375 caseGroup.findFirstToken(TokenTypes.SLIST); 376 isTerminated = caseBody != null && isTerminated(caseBody, false, useContinue); 377 caseGroup = caseGroup.getNextSibling(); 378 } 379 return isTerminated; 380 } 381 382 /** 383 * Checks if a given synchronized block terminated by return, throw or, 384 * if allowed break, continue. 385 * 386 * @param synchronizedAst synchronized block to check. 387 * @param useBreak should we consider break as terminator. 388 * @param useContinue should we consider continue as terminator. 389 * @return true if synchronized block is terminated. 390 */ 391 private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak, 392 boolean useContinue) { 393 return isTerminated( 394 synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue); 395 } 396 397 /** 398 * Determines if the fall through case between {@code currentCase} and 399 * {@code nextCase} is relieved by a appropriate comment. 400 * 401 * @param currentCase AST of the case that falls through to the next case. 402 * @param nextCase AST of the next case. 403 * @return True if a relief comment was found 404 */ 405 private boolean hasFallThroughComment(DetailAST currentCase, DetailAST nextCase) { 406 boolean allThroughComment = false; 407 final int endLineNo = nextCase.getLineNo(); 408 final int endColNo = nextCase.getColumnNo(); 409 410 // Remember: The lines number returned from the AST is 1-based, but 411 // the lines number in this array are 0-based. So you will often 412 // see a "lineNo-1" etc. 413 final String[] lines = getLines(); 414 415 // Handle: 416 // case 1: 417 // /+ FALLTHRU +/ case 2: 418 // .... 419 // and 420 // switch(i) { 421 // default: 422 // /+ FALLTHRU +/} 423 // 424 final String linePart = lines[endLineNo - 1].substring(0, endColNo); 425 if (matchesComment(reliefPattern, linePart, endLineNo)) { 426 allThroughComment = true; 427 } 428 else { 429 // Handle: 430 // case 1: 431 // ..... 432 // // FALLTHRU 433 // case 2: 434 // .... 435 // and 436 // switch(i) { 437 // default: 438 // // FALLTHRU 439 // } 440 final int startLineNo = currentCase.getLineNo(); 441 for (int i = endLineNo - 2; i > startLineNo - 1; i--) { 442 if (!CommonUtil.isBlank(lines[i])) { 443 allThroughComment = matchesComment(reliefPattern, lines[i], i + 1); 444 break; 445 } 446 } 447 } 448 return allThroughComment; 449 } 450 451 /** 452 * Does a regular expression match on the given line and checks that a 453 * possible match is within a comment. 454 * 455 * @param pattern The regular expression pattern to use. 456 * @param line The line of test to do the match on. 457 * @param lineNo The line number in the file. 458 * @return True if a match was found inside a comment. 459 */ 460 private boolean matchesComment(Pattern pattern, String line, int lineNo) { 461 final Matcher matcher = pattern.matcher(line); 462 boolean matches = false; 463 464 if (matcher.find()) { 465 matches = getFileContents().hasIntersectionWithComment(lineNo, matcher.start(), 466 lineNo, matcher.end()); 467 } 468 return matches; 469 } 470 471}