Coverage Report - com.puppycrawl.tools.checkstyle.checks.coding.FallThroughCheck
 
Classes in this File Line Coverage Branch Coverage Complexity
FallThroughCheck
97%
87/89
87%
54/62
4.133
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2014  Oliver Burn
 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  
 package com.puppycrawl.tools.checkstyle.checks.coding;
 20  
 
 21  
 import java.util.regex.Matcher;
 22  
 import java.util.regex.Pattern;
 23  
 
 24  
 import com.puppycrawl.tools.checkstyle.api.Check;
 25  
 import com.puppycrawl.tools.checkstyle.api.DetailAST;
 26  
 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
 27  
 import com.puppycrawl.tools.checkstyle.api.Utils;
 28  
 
 29  
 /**
 30  
  * Checks for fall through in switch statements
 31  
  * Finds locations where a case contains Java code -
 32  
  * but lacks a break, return, throw or continue statement.
 33  
  *
 34  
  * <p>
 35  
  * The check honors special comments to suppress warnings about
 36  
  * the fall through. By default the comments "fallthru",
 37  
  * "fall through", "falls through" and "fallthrough" are recognized.
 38  
  * </p>
 39  
  * <p>
 40  
  * The following fragment of code will NOT trigger the check,
 41  
  * because of the comment "fallthru".
 42  
  * </p>
 43  
  * <pre>
 44  
  * case 3:
 45  
  *     x = 2;
 46  
  *     // fallthru
 47  
  * case 4:
 48  
  * </pre>
 49  
  * <p>
 50  
  * The recognized relief comment can be configured with the property
 51  
  * <code>reliefPattern</code>. Default value of this regular expression
 52  
  * is "fallthru|fall through|fallthrough|falls through".
 53  
  * </p>
 54  
  * <p>
 55  
  * An example of how to configure the check is:
 56  
  * </p>
 57  
  * <pre>
 58  
  * &lt;module name="FallThrough"&gt;
 59  
  *     &lt;property name=&quot;reliefPattern&quot;
 60  
  *                  value=&quot;Fall Through&quot;/&gt;
 61  
  * &lt;/module&gt;
 62  
  * </pre>
 63  
  *
 64  
  * @author o_sukhodolsky
 65  
  */
 66  
 public class FallThroughCheck extends Check
 67  
 {
 68  
     /** Do we need to check last case group. */
 69  
     private boolean mCheckLastGroup;
 70  
 
 71  
     /** Relief pattern to allow fall throught to the next case branch. */
 72  3
     private String mReliefPattern = "fallthru|falls? ?through";
 73  
 
 74  
     /** Relief regexp. */
 75  
     private Pattern mRegExp;
 76  
 
 77  
     /** Creates new instance of the check. */
 78  
     public FallThroughCheck()
 79  3
     {
 80  
         // do nothing
 81  3
     }
 82  
 
 83  
     @Override
 84  
     public int[] getDefaultTokens()
 85  
     {
 86  3
         return new int[]{TokenTypes.CASE_GROUP};
 87  
     }
 88  
 
 89  
     @Override
 90  
     public int[] getRequiredTokens()
 91  
     {
 92  0
         return getDefaultTokens();
 93  
     }
 94  
 
 95  
     /**
 96  
      * Set the relief pattern.
 97  
      *
 98  
      * @param aPattern
 99  
      *            The regular expression pattern.
 100  
      */
 101  
     public void setReliefPattern(String aPattern)
 102  
     {
 103  1
         mReliefPattern = aPattern;
 104  1
     }
 105  
 
 106  
     /**
 107  
      * Configures whether we need to check last case group or not.
 108  
      * @param aValue new value of the property.
 109  
      */
 110  
     public void setCheckLastCaseGroup(boolean aValue)
 111  
     {
 112  1
         mCheckLastGroup = aValue;
 113  1
     }
 114  
 
 115  
     @Override
 116  
     public void init()
 117  
     {
 118  3
         super.init();
 119  3
         mRegExp = Utils.getPattern(mReliefPattern);
 120  3
     }
 121  
 
 122  
     @Override
 123  
     public void visitToken(DetailAST aAST)
 124  
     {
 125  285
         final DetailAST nextGroup = aAST.getNextSibling();
 126  285
         final boolean isLastGroup =
 127  
             ((nextGroup == null)
 128  
              || (nextGroup.getType() != TokenTypes.CASE_GROUP));
 129  285
         if (isLastGroup && !mCheckLastGroup) {
 130  
             // we do not need to check last group
 131  30
             return;
 132  
         }
 133  
 
 134  255
         final DetailAST slist = aAST.findFirstToken(TokenTypes.SLIST);
 135  
 
 136  255
         if (!isTerminated(slist, true, true)
 137  
             && !hasFallTruComment(aAST, nextGroup))
 138  
         {
 139  50
             if (!isLastGroup) {
 140  48
                 log(nextGroup, "fall.through");
 141  
             }
 142  
             else {
 143  2
                 log(aAST, "fall.through.last");
 144  
             }
 145  
         }
 146  255
     }
 147  
 
 148  
     /**
 149  
      * Checks if a given subtree terminated by return, throw or,
 150  
      * if allowed break, continue.
 151  
      * @param aAST root of given subtree
 152  
      * @param aUseBreak should we consider break as terminator.
 153  
      * @param aUseContinue should we consider continue as terminator.
 154  
      * @return true if the subtree is terminated.
 155  
      */
 156  
     private boolean isTerminated(final DetailAST aAST, boolean aUseBreak,
 157  
                                  boolean aUseContinue)
 158  
     {
 159  764
         switch (aAST.getType()) {
 160  
         case TokenTypes.LITERAL_RETURN:
 161  
         case TokenTypes.LITERAL_THROW:
 162  82
             return true;
 163  
         case TokenTypes.LITERAL_BREAK:
 164  93
             return aUseBreak;
 165  
         case TokenTypes.LITERAL_CONTINUE:
 166  36
             return aUseContinue;
 167  
         case TokenTypes.SLIST:
 168  396
             return checkSlist(aAST, aUseBreak, aUseContinue);
 169  
         case TokenTypes.LITERAL_IF:
 170  18
             return checkIf(aAST, aUseBreak, aUseContinue);
 171  
         case TokenTypes.LITERAL_FOR:
 172  
         case TokenTypes.LITERAL_WHILE:
 173  
         case TokenTypes.LITERAL_DO:
 174  24
             return checkLoop(aAST);
 175  
         case TokenTypes.LITERAL_TRY:
 176  24
             return checkTry(aAST, aUseBreak, aUseContinue);
 177  
         case TokenTypes.LITERAL_SWITCH:
 178  12
             return checkSwitch(aAST, aUseContinue);
 179  
         default:
 180  79
             return false;
 181  
         }
 182  
     }
 183  
 
 184  
     /**
 185  
      * Checks if a given SLIST terminated by return, throw or,
 186  
      * if allowed break, continue.
 187  
      * @param aAST SLIST to check
 188  
      * @param aUseBreak should we consider break as terminator.
 189  
      * @param aUseContinue should we consider continue as terminator.
 190  
      * @return true if SLIST is terminated.
 191  
      */
 192  
     private boolean checkSlist(final DetailAST aAST, boolean aUseBreak,
 193  
                                boolean aUseContinue)
 194  
     {
 195  396
         DetailAST lastStmt = aAST.getLastChild();
 196  396
         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  1
             return true;
 200  
         }
 201  
 
 202  395
         if (lastStmt.getType() == TokenTypes.RCURLY) {
 203  111
             lastStmt = lastStmt.getPreviousSibling();
 204  
         }
 205  
 
 206  395
         return (lastStmt != null)
 207  
             && isTerminated(lastStmt, aUseBreak, aUseContinue);
 208  
     }
 209  
 
 210  
     /**
 211  
      * Checks if a given IF terminated by return, throw or,
 212  
      * if allowed break, continue.
 213  
      * @param aAST IF to check
 214  
      * @param aUseBreak should we consider break as terminator.
 215  
      * @param aUseContinue should we consider continue as terminator.
 216  
      * @return true if IF is terminated.
 217  
      */
 218  
     private boolean checkIf(final DetailAST aAST, boolean aUseBreak,
 219  
                             boolean aUseContinue)
 220  
     {
 221  18
         final DetailAST thenStmt = aAST.findFirstToken(TokenTypes.RPAREN)
 222  
                 .getNextSibling();
 223  18
         final DetailAST elseStmt = thenStmt.getNextSibling();
 224  18
         boolean isTerminated = isTerminated(thenStmt, aUseBreak, aUseContinue);
 225  
 
 226  18
         if (isTerminated && (elseStmt != null)) {
 227  12
             isTerminated = isTerminated(elseStmt.getFirstChild(),
 228  
                                         aUseBreak, aUseContinue);
 229  
         }
 230  18
         return isTerminated;
 231  
     }
 232  
 
 233  
     /**
 234  
      * Checks if a given loop terminated by return, throw or,
 235  
      * if allowed break, continue.
 236  
      * @param aAST loop to check
 237  
      * @return true if loop is terminated.
 238  
      */
 239  
     private boolean checkLoop(final DetailAST aAST)
 240  
     {
 241  24
         DetailAST loopBody = null;
 242  24
         if (aAST.getType() == TokenTypes.LITERAL_DO) {
 243  6
             final DetailAST lparen = aAST.findFirstToken(TokenTypes.DO_WHILE);
 244  6
             loopBody = lparen.getPreviousSibling();
 245  6
         }
 246  
         else {
 247  18
             final DetailAST rparen = aAST.findFirstToken(TokenTypes.RPAREN);
 248  18
             loopBody = rparen.getNextSibling();
 249  
         }
 250  24
         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 aAST loop to check
 257  
      * @param aUseBreak should we consider break as terminator.
 258  
      * @param aUseContinue should we consider continue as terminator.
 259  
      * @return true if try/cath/finally block is terminated.
 260  
      */
 261  
     private boolean checkTry(final DetailAST aAST, boolean aUseBreak,
 262  
                              boolean aUseContinue)
 263  
     {
 264  24
         final DetailAST finalStmt = aAST.getLastChild();
 265  24
         if (finalStmt.getType() == TokenTypes.LITERAL_FINALLY) {
 266  12
             return isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST),
 267  
                                 aUseBreak, aUseContinue);
 268  
         }
 269  
 
 270  12
         boolean isTerminated = isTerminated(aAST.getFirstChild(),
 271  
                                             aUseBreak, aUseContinue);
 272  
 
 273  12
         DetailAST catchStmt = aAST.findFirstToken(TokenTypes.LITERAL_CATCH);
 274  30
         while ((catchStmt != null) && isTerminated) {
 275  18
             final DetailAST catchBody =
 276  
                 catchStmt.findFirstToken(TokenTypes.SLIST);
 277  18
             isTerminated &= isTerminated(catchBody, aUseBreak, aUseContinue);
 278  18
             catchStmt = catchStmt.getNextSibling();
 279  18
         }
 280  12
         return isTerminated;
 281  
     }
 282  
 
 283  
     /**
 284  
      * Checks if a given switch terminated by return, throw or,
 285  
      * if allowed break, continue.
 286  
      * @param aAST loop to check
 287  
      * @param aUseContinue should we consider continue as terminator.
 288  
      * @return true if switch is terminated.
 289  
      */
 290  
     private boolean checkSwitch(final DetailAST aAST, boolean aUseContinue)
 291  
     {
 292  12
         DetailAST caseGroup = aAST.findFirstToken(TokenTypes.CASE_GROUP);
 293  12
         boolean isTerminated = (caseGroup != null);
 294  
         while (isTerminated && (caseGroup != null)
 295  42
                && (caseGroup.getType() != TokenTypes.RCURLY))
 296  
         {
 297  30
             final DetailAST caseBody =
 298  
                 caseGroup.findFirstToken(TokenTypes.SLIST);
 299  30
             isTerminated &= isTerminated(caseBody, false, aUseContinue);
 300  30
             caseGroup = caseGroup.getNextSibling();
 301  30
         }
 302  12
         return isTerminated;
 303  
     }
 304  
 
 305  
     /**
 306  
      * Determines if the fall through case between <code>aCurrentCase</code> and
 307  
      * <code>aNextCase</code> is reliefed by a appropriate comment.
 308  
      *
 309  
      * @param aCurrentCase AST of the case that falls through to the next case.
 310  
      * @param aNextCase AST of the next case.
 311  
      * @return True if a relief comment was found
 312  
      */
 313  
     private boolean hasFallTruComment(DetailAST aCurrentCase,
 314  
             DetailAST aNextCase)
 315  
     {
 316  
 
 317  103
         final int startLineNo = aCurrentCase.getLineNo();
 318  103
         final int endLineNo = aNextCase.getLineNo();
 319  103
         final int endColNo = aNextCase.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  103
         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  103
         final String linepart = lines[endLineNo - 1].substring(0, endColNo);
 339  103
         if (commentMatch(mRegExp, linepart, endLineNo)) {
 340  12
             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  106
         for (int i = endLineNo - 2; i > startLineNo - 1; i--) {
 357  106
             if (lines[i].trim().length() != 0) {
 358  91
                 return commentMatch(mRegExp, lines[i], i + 1);
 359  
             }
 360  
         }
 361  
 
 362  
         // Well -- no relief comment found.
 363  0
         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 aPattern The regular expression pattern to use.
 370  
      * @param aLine The line of test to do the match on.
 371  
      * @param aLineNo The line number in the file.
 372  
      * @return True if a match was found inside a comment.
 373  
      */
 374  
     private boolean commentMatch(Pattern aPattern, String aLine, int aLineNo
 375  
     )
 376  
     {
 377  194
         final Matcher matcher = aPattern.matcher(aLine);
 378  
 
 379  194
         final boolean hit = matcher.find();
 380  
 
 381  194
         if (hit) {
 382  53
             final int startMatch = matcher.start();
 383  
             // -1 because it returns the char position beyond the match
 384  53
             final int endMatch = matcher.end() - 1;
 385  53
             return getFileContents().hasIntersectionWithComment(aLineNo,
 386  
                     startMatch, aLineNo, endMatch);
 387  
         }
 388  141
         return false;
 389  
     }
 390  
 }