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.CommonUtils; 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 * @author o_sukhodolsky 071 */ 072@StatelessCheck 073public class FallThroughCheck extends AbstractCheck { 074 075 /** 076 * A key is pointing to the warning message text in "messages.properties" 077 * file. 078 */ 079 public static final String MSG_FALL_THROUGH = "fall.through"; 080 081 /** 082 * A key is pointing to the warning message text in "messages.properties" 083 * file. 084 */ 085 public static final String MSG_FALL_THROUGH_LAST = "fall.through.last"; 086 087 /** Do we need to check last case group. */ 088 private boolean checkLastCaseGroup; 089 090 /** Relief regexp to allow fall through to the next case branch. */ 091 private Pattern reliefPattern = Pattern.compile("fallthru|falls? ?through"); 092 093 @Override 094 public int[] getDefaultTokens() { 095 return getRequiredTokens(); 096 } 097 098 @Override 099 public int[] getRequiredTokens() { 100 return new int[] {TokenTypes.CASE_GROUP}; 101 } 102 103 @Override 104 public int[] getAcceptableTokens() { 105 return getRequiredTokens(); 106 } 107 108 /** 109 * Set the relief pattern. 110 * 111 * @param pattern 112 * The regular expression pattern. 113 */ 114 public void setReliefPattern(Pattern 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 visitToken(DetailAST ast) { 128 final DetailAST nextGroup = ast.getNextSibling(); 129 final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP; 130 if (!isLastGroup || checkLastCaseGroup) { 131 final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST); 132 133 if (slist != null && !isTerminated(slist, true, true) 134 && !hasFallThroughComment(ast, nextGroup)) { 135 if (isLastGroup) { 136 log(ast, MSG_FALL_THROUGH_LAST); 137 } 138 else { 139 log(nextGroup, MSG_FALL_THROUGH); 140 } 141 } 142 } 143 } 144 145 /** 146 * Checks if a given subtree terminated by return, throw or, 147 * if allowed break, continue. 148 * @param ast root of given subtree 149 * @param useBreak should we consider break as terminator. 150 * @param useContinue should we consider continue as terminator. 151 * @return true if the subtree is terminated. 152 */ 153 private boolean isTerminated(final DetailAST ast, boolean useBreak, 154 boolean useContinue) { 155 final boolean terminated; 156 157 switch (ast.getType()) { 158 case TokenTypes.LITERAL_RETURN: 159 case TokenTypes.LITERAL_THROW: 160 terminated = true; 161 break; 162 case TokenTypes.LITERAL_BREAK: 163 terminated = useBreak; 164 break; 165 case TokenTypes.LITERAL_CONTINUE: 166 terminated = useContinue; 167 break; 168 case TokenTypes.SLIST: 169 terminated = checkSlist(ast, useBreak, useContinue); 170 break; 171 case TokenTypes.LITERAL_IF: 172 terminated = checkIf(ast, useBreak, useContinue); 173 break; 174 case TokenTypes.LITERAL_FOR: 175 case TokenTypes.LITERAL_WHILE: 176 case TokenTypes.LITERAL_DO: 177 terminated = checkLoop(ast); 178 break; 179 case TokenTypes.LITERAL_TRY: 180 terminated = checkTry(ast, useBreak, useContinue); 181 break; 182 case TokenTypes.LITERAL_SWITCH: 183 terminated = checkSwitch(ast, useContinue); 184 break; 185 case TokenTypes.LITERAL_SYNCHRONIZED: 186 terminated = checkSynchronized(ast, useBreak, useContinue); 187 break; 188 default: 189 terminated = false; 190 } 191 return terminated; 192 } 193 194 /** 195 * Checks if a given SLIST terminated by return, throw or, 196 * if allowed break, continue. 197 * @param slistAst SLIST to check 198 * @param useBreak should we consider break as terminator. 199 * @param useContinue should we consider continue as terminator. 200 * @return true if SLIST is terminated. 201 */ 202 private boolean checkSlist(final DetailAST slistAst, boolean useBreak, 203 boolean useContinue) { 204 DetailAST lastStmt = slistAst.getLastChild(); 205 206 if (lastStmt.getType() == TokenTypes.RCURLY) { 207 lastStmt = lastStmt.getPreviousSibling(); 208 } 209 210 return lastStmt != null 211 && isTerminated(lastStmt, useBreak, useContinue); 212 } 213 214 /** 215 * Checks if a given IF terminated by return, throw or, 216 * if allowed break, continue. 217 * @param ast IF to check 218 * @param useBreak should we consider break as terminator. 219 * @param useContinue should we consider continue as terminator. 220 * @return true if IF is terminated. 221 */ 222 private boolean checkIf(final DetailAST ast, boolean useBreak, 223 boolean useContinue) { 224 final DetailAST thenStmt = ast.findFirstToken(TokenTypes.RPAREN) 225 .getNextSibling(); 226 final DetailAST elseStmt = thenStmt.getNextSibling(); 227 boolean isTerminated = isTerminated(thenStmt, useBreak, useContinue); 228 229 if (isTerminated && elseStmt != null) { 230 isTerminated = isTerminated(elseStmt.getFirstChild(), 231 useBreak, useContinue); 232 } 233 else if (elseStmt == null) { 234 isTerminated = false; 235 } 236 return isTerminated; 237 } 238 239 /** 240 * Checks if a given loop terminated by return, throw or, 241 * if allowed break, continue. 242 * @param ast loop to check 243 * @return true if loop is terminated. 244 */ 245 private boolean checkLoop(final DetailAST ast) { 246 final DetailAST loopBody; 247 if (ast.getType() == TokenTypes.LITERAL_DO) { 248 final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE); 249 loopBody = lparen.getPreviousSibling(); 250 } 251 else { 252 final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN); 253 loopBody = rparen.getNextSibling(); 254 } 255 return isTerminated(loopBody, false, false); 256 } 257 258 /** 259 * Checks if a given try/catch/finally block terminated by return, throw or, 260 * if allowed break, continue. 261 * @param ast loop to check 262 * @param useBreak should we consider break as terminator. 263 * @param useContinue should we consider continue as terminator. 264 * @return true if try/catch/finally block is terminated. 265 */ 266 private boolean checkTry(final DetailAST ast, boolean useBreak, 267 boolean useContinue) { 268 final DetailAST finalStmt = ast.getLastChild(); 269 boolean isTerminated = false; 270 if (finalStmt.getType() == TokenTypes.LITERAL_FINALLY) { 271 isTerminated = isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST), 272 useBreak, useContinue); 273 } 274 275 if (!isTerminated) { 276 DetailAST firstChild = ast.getFirstChild(); 277 278 if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) { 279 firstChild = firstChild.getNextSibling(); 280 } 281 282 isTerminated = isTerminated(firstChild, 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 * Checks if a given synchronized block terminated by return, throw or, 319 * if allowed break, continue. 320 * @param synchronizedAst synchronized block to check. 321 * @param useBreak should we consider break as terminator. 322 * @param useContinue should we consider continue as terminator. 323 * @return true if synchronized block is terminated. 324 */ 325 private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak, 326 boolean useContinue) { 327 return isTerminated( 328 synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue); 329 } 330 331 /** 332 * Determines if the fall through case between {@code currentCase} and 333 * {@code nextCase} is relieved by a appropriate comment. 334 * 335 * @param currentCase AST of the case that falls through to the next case. 336 * @param nextCase AST of the next case. 337 * @return True if a relief comment was found 338 */ 339 private boolean hasFallThroughComment(DetailAST currentCase, DetailAST nextCase) { 340 boolean allThroughComment = false; 341 final int endLineNo = nextCase.getLineNo(); 342 final int endColNo = nextCase.getColumnNo(); 343 344 // Remember: The lines number returned from the AST is 1-based, but 345 // the lines number in this array are 0-based. So you will often 346 // see a "lineNo-1" etc. 347 final String[] lines = getLines(); 348 349 // Handle: 350 // case 1: 351 // /+ FALLTHRU +/ case 2: 352 // .... 353 // and 354 // switch(i) { 355 // default: 356 // /+ FALLTHRU +/} 357 // 358 final String linePart = lines[endLineNo - 1].substring(0, endColNo); 359 if (matchesComment(reliefPattern, linePart, endLineNo)) { 360 allThroughComment = true; 361 } 362 else { 363 // Handle: 364 // case 1: 365 // ..... 366 // // FALLTHRU 367 // case 2: 368 // .... 369 // and 370 // switch(i) { 371 // default: 372 // // FALLTHRU 373 // } 374 final int startLineNo = currentCase.getLineNo(); 375 for (int i = endLineNo - 2; i > startLineNo - 1; i--) { 376 if (!CommonUtils.isBlank(lines[i])) { 377 allThroughComment = matchesComment(reliefPattern, lines[i], i + 1); 378 break; 379 } 380 } 381 } 382 return allThroughComment; 383 } 384 385 /** 386 * Does a regular expression match on the given line and checks that a 387 * possible match is within a comment. 388 * @param pattern The regular expression pattern to use. 389 * @param line The line of test to do the match on. 390 * @param lineNo The line number in the file. 391 * @return True if a match was found inside a comment. 392 */ 393 private boolean matchesComment(Pattern pattern, String line, int lineNo) { 394 final Matcher matcher = pattern.matcher(line); 395 boolean matches = false; 396 397 if (matcher.find()) { 398 // -1 because it returns the char position beyond the match 399 matches = getFileContents().hasIntersectionWithComment(lineNo, matcher.start(), 400 lineNo, matcher.end() - 1); 401 } 402 return matches; 403 } 404 405}