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