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