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