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