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