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; 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 AbstractCheck { 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 regexp to allow fall through to the next case branch. */ 088 private Pattern reliefPattern = Pattern.compile("fallthru|falls? ?through"); 089 090 @Override 091 public int[] getDefaultTokens() { 092 return new int[] {TokenTypes.CASE_GROUP}; 093 } 094 095 @Override 096 public int[] getRequiredTokens() { 097 return getDefaultTokens(); 098 } 099 100 @Override 101 public int[] getAcceptableTokens() { 102 return new int[] {TokenTypes.CASE_GROUP}; 103 } 104 105 /** 106 * Set the relief pattern. 107 * 108 * @param pattern 109 * The regular expression pattern. 110 */ 111 public void setReliefPattern(Pattern pattern) { 112 reliefPattern = pattern; 113 } 114 115 /** 116 * Configures whether we need to check last case group or not. 117 * @param value new value of the property. 118 */ 119 public void setCheckLastCaseGroup(boolean value) { 120 checkLastCaseGroup = value; 121 } 122 123 @Override 124 public void visitToken(DetailAST ast) { 125 final DetailAST nextGroup = ast.getNextSibling(); 126 final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP; 127 if (!isLastGroup || checkLastCaseGroup) { 128 final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST); 129 130 if (slist != null && !isTerminated(slist, true, true) 131 && !hasFallThroughComment(ast, nextGroup)) { 132 if (isLastGroup) { 133 log(ast, MSG_FALL_THROUGH_LAST); 134 } 135 else { 136 log(nextGroup, MSG_FALL_THROUGH); 137 } 138 } 139 } 140 } 141 142 /** 143 * Checks if a given subtree terminated by return, throw or, 144 * if allowed break, continue. 145 * @param ast root of given subtree 146 * @param useBreak should we consider break as terminator. 147 * @param useContinue should we consider continue as terminator. 148 * @return true if the subtree is terminated. 149 */ 150 private boolean isTerminated(final DetailAST ast, boolean useBreak, 151 boolean useContinue) { 152 final boolean terminated; 153 154 switch (ast.getType()) { 155 case TokenTypes.LITERAL_RETURN: 156 case TokenTypes.LITERAL_THROW: 157 terminated = true; 158 break; 159 case TokenTypes.LITERAL_BREAK: 160 terminated = useBreak; 161 break; 162 case TokenTypes.LITERAL_CONTINUE: 163 terminated = useContinue; 164 break; 165 case TokenTypes.SLIST: 166 terminated = checkSlist(ast, useBreak, useContinue); 167 break; 168 case TokenTypes.LITERAL_IF: 169 terminated = checkIf(ast, useBreak, useContinue); 170 break; 171 case TokenTypes.LITERAL_FOR: 172 case TokenTypes.LITERAL_WHILE: 173 case TokenTypes.LITERAL_DO: 174 terminated = checkLoop(ast); 175 break; 176 case TokenTypes.LITERAL_TRY: 177 terminated = checkTry(ast, useBreak, useContinue); 178 break; 179 case TokenTypes.LITERAL_SWITCH: 180 terminated = checkSwitch(ast, useContinue); 181 break; 182 default: 183 terminated = false; 184 } 185 return terminated; 186 } 187 188 /** 189 * Checks if a given SLIST terminated by return, throw or, 190 * if allowed break, continue. 191 * @param slistAst SLIST to check 192 * @param useBreak should we consider break as terminator. 193 * @param useContinue should we consider continue as terminator. 194 * @return true if SLIST is terminated. 195 */ 196 private boolean checkSlist(final DetailAST slistAst, boolean useBreak, 197 boolean useContinue) { 198 DetailAST lastStmt = slistAst.getLastChild(); 199 200 if (lastStmt.getType() == TokenTypes.RCURLY) { 201 lastStmt = lastStmt.getPreviousSibling(); 202 } 203 204 return lastStmt != null 205 && isTerminated(lastStmt, useBreak, useContinue); 206 } 207 208 /** 209 * Checks if a given IF terminated by return, throw or, 210 * if allowed break, continue. 211 * @param ast IF to check 212 * @param useBreak should we consider break as terminator. 213 * @param useContinue should we consider continue as terminator. 214 * @return true if IF is terminated. 215 */ 216 private boolean checkIf(final DetailAST ast, boolean useBreak, 217 boolean useContinue) { 218 final DetailAST thenStmt = ast.findFirstToken(TokenTypes.RPAREN) 219 .getNextSibling(); 220 final DetailAST elseStmt = thenStmt.getNextSibling(); 221 boolean isTerminated = isTerminated(thenStmt, useBreak, useContinue); 222 223 if (isTerminated && elseStmt != null) { 224 isTerminated = isTerminated(elseStmt.getFirstChild(), 225 useBreak, useContinue); 226 } 227 else if (elseStmt == null) { 228 isTerminated = false; 229 } 230 return isTerminated; 231 } 232 233 /** 234 * Checks if a given loop terminated by return, throw or, 235 * if allowed break, continue. 236 * @param ast loop to check 237 * @return true if loop is terminated. 238 */ 239 private boolean checkLoop(final DetailAST ast) { 240 final DetailAST loopBody; 241 if (ast.getType() == TokenTypes.LITERAL_DO) { 242 final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE); 243 loopBody = lparen.getPreviousSibling(); 244 } 245 else { 246 final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN); 247 loopBody = rparen.getNextSibling(); 248 } 249 return isTerminated(loopBody, false, false); 250 } 251 252 /** 253 * Checks if a given try/catch/finally block terminated by return, throw or, 254 * if allowed break, continue. 255 * @param ast loop to check 256 * @param useBreak should we consider break as terminator. 257 * @param useContinue should we consider continue as terminator. 258 * @return true if try/catch/finally block is terminated. 259 */ 260 private boolean checkTry(final DetailAST ast, boolean useBreak, 261 boolean useContinue) { 262 final DetailAST finalStmt = ast.getLastChild(); 263 boolean isTerminated = false; 264 if (finalStmt.getType() == TokenTypes.LITERAL_FINALLY) { 265 isTerminated = isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST), 266 useBreak, useContinue); 267 } 268 269 if (!isTerminated) { 270 DetailAST firstChild = ast.getFirstChild(); 271 272 if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) { 273 firstChild = firstChild.getNextSibling(); 274 } 275 276 isTerminated = isTerminated(firstChild, 277 useBreak, useContinue); 278 279 DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH); 280 while (catchStmt != null 281 && isTerminated 282 && catchStmt.getType() == TokenTypes.LITERAL_CATCH) { 283 final DetailAST catchBody = 284 catchStmt.findFirstToken(TokenTypes.SLIST); 285 isTerminated = isTerminated(catchBody, useBreak, useContinue); 286 catchStmt = catchStmt.getNextSibling(); 287 } 288 } 289 return isTerminated; 290 } 291 292 /** 293 * Checks if a given switch terminated by return, throw or, 294 * if allowed break, continue. 295 * @param literalSwitchAst loop to check 296 * @param useContinue should we consider continue as terminator. 297 * @return true if switch is terminated. 298 */ 299 private boolean checkSwitch(final DetailAST literalSwitchAst, boolean useContinue) { 300 DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP); 301 boolean isTerminated = caseGroup != null; 302 while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) { 303 final DetailAST caseBody = 304 caseGroup.findFirstToken(TokenTypes.SLIST); 305 isTerminated = caseBody != null && isTerminated(caseBody, false, useContinue); 306 caseGroup = caseGroup.getNextSibling(); 307 } 308 return isTerminated; 309 } 310 311 /** 312 * Determines if the fall through case between {@code currentCase} and 313 * {@code nextCase} is relieved by a appropriate comment. 314 * 315 * @param currentCase AST of the case that falls through to the next case. 316 * @param nextCase AST of the next case. 317 * @return True if a relief comment was found 318 */ 319 private boolean hasFallThroughComment(DetailAST currentCase, DetailAST nextCase) { 320 boolean allThroughComment = false; 321 final int endLineNo = nextCase.getLineNo(); 322 final int endColNo = nextCase.getColumnNo(); 323 324 // Remember: The lines number returned from the AST is 1-based, but 325 // the lines number in this array are 0-based. So you will often 326 // see a "lineNo-1" etc. 327 final String[] lines = getLines(); 328 329 // Handle: 330 // case 1: 331 // /+ FALLTHRU +/ case 2: 332 // .... 333 // and 334 // switch(i) { 335 // default: 336 // /+ FALLTHRU +/} 337 // 338 final String linePart = lines[endLineNo - 1].substring(0, endColNo); 339 if (matchesComment(reliefPattern, linePart, endLineNo)) { 340 allThroughComment = true; 341 } 342 else { 343 // Handle: 344 // case 1: 345 // ..... 346 // // FALLTHRU 347 // case 2: 348 // .... 349 // and 350 // switch(i) { 351 // default: 352 // // FALLTHRU 353 // } 354 final int startLineNo = currentCase.getLineNo(); 355 for (int i = endLineNo - 2; i > startLineNo - 1; i--) { 356 if (!lines[i].trim().isEmpty()) { 357 allThroughComment = matchesComment(reliefPattern, lines[i], i + 1); 358 break; 359 } 360 } 361 } 362 return allThroughComment; 363 } 364 365 /** 366 * Does a regular expression match on the given line and checks that a 367 * possible match is within a comment. 368 * @param pattern The regular expression pattern to use. 369 * @param line The line of test to do the match on. 370 * @param lineNo The line number in the file. 371 * @return True if a match was found inside a comment. 372 */ 373 private boolean matchesComment(Pattern pattern, String line, int lineNo 374 ) { 375 final Matcher matcher = pattern.matcher(line); 376 377 final boolean hit = matcher.find(); 378 379 if (hit) { 380 final int startMatch = matcher.start(); 381 // -1 because it returns the char position beyond the match 382 final int endMatch = matcher.end() - 1; 383 return getFileContents().hasIntersectionWithComment(lineNo, 384 startMatch, lineNo, endMatch); 385 } 386 return false; 387 } 388}