Coverage Report - com.puppycrawl.tools.checkstyle.checks.coding.FallThroughCheck
 
Classes in this File Line Coverage Branch Coverage Complexity
FallThroughCheck
100%
109/109
100%
68/68
3.467
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2017 the original author or authors.
 4  
 //
 5  
 // This library is free software; you can redistribute it and/or
 6  
 // modify it under the terms of the GNU Lesser General Public
 7  
 // License as published by the Free Software Foundation; either
 8  
 // version 2.1 of the License, or (at your option) any later version.
 9  
 //
 10  
 // This library is distributed in the hope that it will be useful,
 11  
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 13  
 // Lesser General Public License for more details.
 14  
 //
 15  
 // You should have received a copy of the GNU Lesser General Public
 16  
 // License along with this library; if not, write to the Free Software
 17  
 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 18  
 ////////////////////////////////////////////////////////////////////////////////
 19  
 
 20  
 package com.puppycrawl.tools.checkstyle.checks.coding;
 21  
 
 22  
 import java.util.regex.Matcher;
 23  
 import java.util.regex.Pattern;
 24  
 
 25  
 import com.puppycrawl.tools.checkstyle.StatelessCheck;
 26  
 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
 27  
 import com.puppycrawl.tools.checkstyle.api.DetailAST;
 28  
 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
 29  
 import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
 30  
 
 31  
 /**
 32  
  * Checks for fall through in switch statements
 33  
  * Finds locations where a case <b>contains</b> Java code -
 34  
  * but lacks a break, return, throw or continue statement.
 35  
  *
 36  
  * <p>
 37  
  * The check honors special comments to suppress warnings about
 38  
  * the fall through. By default the comments "fallthru",
 39  
  * "fall through", "falls through" and "fallthrough" are recognized.
 40  
  * </p>
 41  
  * <p>
 42  
  * The following fragment of code will NOT trigger the check,
 43  
  * because of the comment "fallthru" and absence of any Java code
 44  
  * in case 5.
 45  
  * </p>
 46  
  * <pre>
 47  
  * case 3:
 48  
  *     x = 2;
 49  
  *     // fallthru
 50  
  * case 4:
 51  
  * case 5:
 52  
  * case 6:
 53  
  *     break;
 54  
  * </pre>
 55  
  * <p>
 56  
  * The recognized relief comment can be configured with the property
 57  
  * {@code reliefPattern}. Default value of this regular expression
 58  
  * is "fallthru|fall through|fallthrough|falls through".
 59  
  * </p>
 60  
  * <p>
 61  
  * An example of how to configure the check is:
 62  
  * </p>
 63  
  * <pre>
 64  
  * &lt;module name="FallThrough"&gt;
 65  
  *     &lt;property name=&quot;reliefPattern&quot;
 66  
  *                  value=&quot;Fall Through&quot;/&gt;
 67  
  * &lt;/module&gt;
 68  
  * </pre>
 69  
  *
 70  
  * @author o_sukhodolsky
 71  
  */
 72  
 @StatelessCheck
 73  15
 public class FallThroughCheck extends AbstractCheck {
 74  
 
 75  
     /**
 76  
      * A key is pointing to the warning message text in "messages.properties"
 77  
      * file.
 78  
      */
 79  
     public static final String MSG_FALL_THROUGH = "fall.through";
 80  
 
 81  
     /**
 82  
      * A key is pointing to the warning message text in "messages.properties"
 83  
      * file.
 84  
      */
 85  
     public static final String MSG_FALL_THROUGH_LAST = "fall.through.last";
 86  
 
 87  
     /** Do we need to check last case group. */
 88  
     private boolean checkLastCaseGroup;
 89  
 
 90  
     /** Relief regexp to allow fall through to the next case branch. */
 91  15
     private Pattern reliefPattern = Pattern.compile("fallthru|falls? ?through");
 92  
 
 93  
     @Override
 94  
     public int[] getDefaultTokens() {
 95  22
         return getRequiredTokens();
 96  
     }
 97  
 
 98  
     @Override
 99  
     public int[] getRequiredTokens() {
 100  50
         return new int[] {TokenTypes.CASE_GROUP};
 101  
     }
 102  
 
 103  
     @Override
 104  
     public int[] getAcceptableTokens() {
 105  6
         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  2
         reliefPattern = pattern;
 116  2
     }
 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  1
         checkLastCaseGroup = value;
 124  1
     }
 125  
 
 126  
     @Override
 127  
     public void visitToken(DetailAST ast) {
 128  374
         final DetailAST nextGroup = ast.getNextSibling();
 129  374
         final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP;
 130  374
         if (!isLastGroup || checkLastCaseGroup) {
 131  327
             final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
 132  
 
 133  327
             if (slist != null && !isTerminated(slist, true, true)
 134  142
                 && !hasFallThroughComment(ast, nextGroup)) {
 135  79
                 if (isLastGroup) {
 136  2
                     log(ast, MSG_FALL_THROUGH_LAST);
 137  
                 }
 138  
                 else {
 139  77
                     log(nextGroup, MSG_FALL_THROUGH);
 140  
                 }
 141  
             }
 142  
         }
 143  374
     }
 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  1051
         switch (ast.getType()) {
 158  
             case TokenTypes.LITERAL_RETURN:
 159  
             case TokenTypes.LITERAL_THROW:
 160  105
                 terminated = true;
 161  105
                 break;
 162  
             case TokenTypes.LITERAL_BREAK:
 163  130
                 terminated = useBreak;
 164  130
                 break;
 165  
             case TokenTypes.LITERAL_CONTINUE:
 166  36
                 terminated = useContinue;
 167  36
                 break;
 168  
             case TokenTypes.SLIST:
 169  551
                 terminated = checkSlist(ast, useBreak, useContinue);
 170  551
                 break;
 171  
             case TokenTypes.LITERAL_IF:
 172  34
                 terminated = checkIf(ast, useBreak, useContinue);
 173  34
                 break;
 174  
             case TokenTypes.LITERAL_FOR:
 175  
             case TokenTypes.LITERAL_WHILE:
 176  
             case TokenTypes.LITERAL_DO:
 177  24
                 terminated = checkLoop(ast);
 178  24
                 break;
 179  
             case TokenTypes.LITERAL_TRY:
 180  54
                 terminated = checkTry(ast, useBreak, useContinue);
 181  54
                 break;
 182  
             case TokenTypes.LITERAL_SWITCH:
 183  18
                 terminated = checkSwitch(ast, useContinue);
 184  18
                 break;
 185  
             case TokenTypes.LITERAL_SYNCHRONIZED:
 186  12
                 terminated = checkSynchronized(ast, useBreak, useContinue);
 187  12
                 break;
 188  
             default:
 189  87
                 terminated = false;
 190  
         }
 191  1051
         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  551
         DetailAST lastStmt = slistAst.getLastChild();
 205  
 
 206  551
         if (lastStmt.getType() == TokenTypes.RCURLY) {
 207  197
             lastStmt = lastStmt.getPreviousSibling();
 208  
         }
 209  
 
 210  1102
         return lastStmt != null
 211  514
             && 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  34
         final DetailAST thenStmt = ast.findFirstToken(TokenTypes.RPAREN)
 225  34
                 .getNextSibling();
 226  34
         final DetailAST elseStmt = thenStmt.getNextSibling();
 227  34
         boolean isTerminated = isTerminated(thenStmt, useBreak, useContinue);
 228  
 
 229  34
         if (isTerminated && elseStmt != null) {
 230  17
             isTerminated = isTerminated(elseStmt.getFirstChild(),
 231  
                 useBreak, useContinue);
 232  
         }
 233  17
         else if (elseStmt == null) {
 234  15
             isTerminated = false;
 235  
         }
 236  34
         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  24
         if (ast.getType() == TokenTypes.LITERAL_DO) {
 248  6
             final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE);
 249  6
             loopBody = lparen.getPreviousSibling();
 250  6
         }
 251  
         else {
 252  18
             final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
 253  18
             loopBody = rparen.getNextSibling();
 254  
         }
 255  24
         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  54
         final DetailAST finalStmt = ast.getLastChild();
 269  54
         boolean isTerminated = false;
 270  54
         if (finalStmt.getType() == TokenTypes.LITERAL_FINALLY) {
 271  24
             isTerminated = isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST),
 272  
                                 useBreak, useContinue);
 273  
         }
 274  
 
 275  54
         if (!isTerminated) {
 276  42
             DetailAST firstChild = ast.getFirstChild();
 277  
 
 278  42
             if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) {
 279  24
                 firstChild = firstChild.getNextSibling();
 280  
             }
 281  
 
 282  42
             isTerminated = isTerminated(firstChild,
 283  
                     useBreak, useContinue);
 284  
 
 285  42
             DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH);
 286  72
             while (catchStmt != null
 287  
                     && isTerminated
 288  36
                     && catchStmt.getType() == TokenTypes.LITERAL_CATCH) {
 289  30
                 final DetailAST catchBody =
 290  30
                         catchStmt.findFirstToken(TokenTypes.SLIST);
 291  30
                 isTerminated = isTerminated(catchBody, useBreak, useContinue);
 292  30
                 catchStmt = catchStmt.getNextSibling();
 293  30
             }
 294  
         }
 295  54
         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  18
         DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP);
 307  18
         boolean isTerminated = caseGroup != null;
 308  51
         while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) {
 309  33
             final DetailAST caseBody =
 310  33
                 caseGroup.findFirstToken(TokenTypes.SLIST);
 311  33
             isTerminated = caseBody != null && isTerminated(caseBody, false, useContinue);
 312  33
             caseGroup = caseGroup.getNextSibling();
 313  33
         }
 314  18
         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  24
         return isTerminated(
 328  12
             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  142
         boolean allThroughComment = false;
 341  142
         final int endLineNo = nextCase.getLineNo();
 342  142
         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  142
         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  142
         final String linePart = lines[endLineNo - 1].substring(0, endColNo);
 359  142
         if (matchesComment(reliefPattern, linePart, endLineNo)) {
 360  12
             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  130
             final int startLineNo = currentCase.getLineNo();
 375  148
             for (int i = endLineNo - 2; i > startLineNo - 1; i--) {
 376  145
                 if (!CommonUtils.isBlank(lines[i])) {
 377  127
                     allThroughComment = matchesComment(reliefPattern, lines[i], i + 1);
 378  127
                     break;
 379  
                 }
 380  
             }
 381  
         }
 382  142
         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  269
         final Matcher matcher = pattern.matcher(line);
 395  269
         boolean matches = false;
 396  
 
 397  269
         if (matcher.find()) {
 398  
             // -1 because it returns the char position beyond the match
 399  126
             matches = getFileContents().hasIntersectionWithComment(lineNo, matcher.start(),
 400  63
                     lineNo, matcher.end() - 1);
 401  
         }
 402  269
         return matches;
 403  
     }
 404  
 }