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