Coverage Report - com.puppycrawl.tools.checkstyle.checks.coding.ModifiedControlVariableCheck
 
Classes in this File Line Coverage Branch Coverage Complexity
ModifiedControlVariableCheck
100%
104/104
100%
44/44
0
 
 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.ArrayDeque;
 23  
 import java.util.Arrays;
 24  
 import java.util.Deque;
 25  
 import java.util.HashSet;
 26  
 import java.util.LinkedList;
 27  
 import java.util.List;
 28  
 import java.util.Set;
 29  
 import java.util.stream.Collectors;
 30  
 
 31  
 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
 32  
 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
 33  
 import com.puppycrawl.tools.checkstyle.api.DetailAST;
 34  
 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
 35  
 
 36  
 /**
 37  
  * Check for ensuring that for loop control variables are not modified
 38  
  * inside the for block. An example is:
 39  
  *
 40  
  * <pre>
 41  
  * {@code
 42  
  * for (int i = 0; i &lt; 1; i++) {
 43  
  *     i++;//violation
 44  
  * }
 45  
  * }
 46  
  * </pre>
 47  
  * Rationale: If the control variable is modified inside the loop
 48  
  * body, the program flow becomes more difficult to follow.<br>
 49  
  * See <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.14">
 50  
  * FOR statement</a> specification for more details.
 51  
  * <p>Examples:</p>
 52  
  *
 53  
  * <pre>
 54  
  * &lt;module name=&quot;ModifiedControlVariable&quot;&gt;
 55  
  * &lt;/module&gt;
 56  
  * </pre>
 57  
  *
 58  
  * <p>Such loop would be suppressed:
 59  
  *
 60  
  * <pre>
 61  
  * {@code
 62  
  * for(int i=0; i &lt; 10;) {
 63  
  *     i++;
 64  
  * }
 65  
  * }
 66  
  * </pre>
 67  
  *
 68  
  * <p>
 69  
  * By default, This Check validates
 70  
  *  <a href = "https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.14.2">
 71  
  * Enhanced For-Loop</a>.
 72  
  * </p>
 73  
  * <p>
 74  
  * Option 'skipEnhancedForLoopVariable' could be used to skip check of variable
 75  
  *  from Enhanced For Loop.
 76  
  * </p>
 77  
  * <p>
 78  
  * An example of how to configure the check so that it skips enhanced For Loop Variable is:
 79  
  * </p>
 80  
  * <pre>
 81  
  * &lt;module name="ModifiedControlVariable"&gt;
 82  
  *     &lt;property name="skipEnhancedForLoopVariable" value="true"/&gt;
 83  
  * &lt;/module&gt;
 84  
  * </pre>
 85  
  * <p>Example:</p>
 86  
  *
 87  
  * <pre>
 88  
  * {@code
 89  
  * for (String line: lines) {
 90  
  *     line = line.trim();   // it will skip this violation
 91  
  * }
 92  
  * }
 93  
  * </pre>
 94  
  *
 95  
  *
 96  
  * @author Daniel Grenner
 97  
  * @author <a href="mailto:piotr.listkiewicz@gmail.com">liscju</a>
 98  
  */
 99  
 @FileStatefulCheck
 100  15
 public final class ModifiedControlVariableCheck extends AbstractCheck {
 101  
 
 102  
     /**
 103  
      * A key is pointing to the warning message text in "messages.properties"
 104  
      * file.
 105  
      */
 106  
     public static final String MSG_KEY = "modified.control.variable";
 107  
 
 108  
     /**
 109  
      * Message thrown with IllegalStateException.
 110  
      */
 111  
     private static final String ILLEGAL_TYPE_OF_TOKEN = "Illegal type of token: ";
 112  
 
 113  
     /** Operations which can change control variable in update part of the loop. */
 114  2
     private static final Set<Integer> MUTATION_OPERATIONS =
 115  1
         Arrays.stream(new Integer[] {
 116  1
             TokenTypes.POST_INC,
 117  1
             TokenTypes.POST_DEC,
 118  1
             TokenTypes.DEC,
 119  1
             TokenTypes.INC,
 120  1
             TokenTypes.ASSIGN,
 121  1
         }).collect(Collectors.toSet());
 122  
 
 123  
     /** Stack of block parameters. */
 124  15
     private final Deque<Deque<String>> variableStack = new ArrayDeque<>();
 125  
 
 126  
     /** Controls whether to skip enhanced for-loop variable. */
 127  
     private boolean skipEnhancedForLoopVariable;
 128  
 
 129  
     /**
 130  
      * Whether to skip enhanced for-loop variable or not.
 131  
      * @param skipEnhancedForLoopVariable whether to skip enhanced for-loop variable
 132  
      */
 133  
     public void setSkipEnhancedForLoopVariable(boolean skipEnhancedForLoopVariable) {
 134  3
         this.skipEnhancedForLoopVariable = skipEnhancedForLoopVariable;
 135  3
     }
 136  
 
 137  
     @Override
 138  
     public int[] getDefaultTokens() {
 139  19
         return getRequiredTokens();
 140  
     }
 141  
 
 142  
     @Override
 143  
     public int[] getRequiredTokens() {
 144  43
         return new int[] {
 145  
             TokenTypes.OBJBLOCK,
 146  
             TokenTypes.LITERAL_FOR,
 147  
             TokenTypes.FOR_ITERATOR,
 148  
             TokenTypes.FOR_EACH_CLAUSE,
 149  
             TokenTypes.ASSIGN,
 150  
             TokenTypes.PLUS_ASSIGN,
 151  
             TokenTypes.MINUS_ASSIGN,
 152  
             TokenTypes.STAR_ASSIGN,
 153  
             TokenTypes.DIV_ASSIGN,
 154  
             TokenTypes.MOD_ASSIGN,
 155  
             TokenTypes.SR_ASSIGN,
 156  
             TokenTypes.BSR_ASSIGN,
 157  
             TokenTypes.SL_ASSIGN,
 158  
             TokenTypes.BAND_ASSIGN,
 159  
             TokenTypes.BXOR_ASSIGN,
 160  
             TokenTypes.BOR_ASSIGN,
 161  
             TokenTypes.INC,
 162  
             TokenTypes.POST_INC,
 163  
             TokenTypes.DEC,
 164  
             TokenTypes.POST_DEC,
 165  
         };
 166  
     }
 167  
 
 168  
     @Override
 169  
     public int[] getAcceptableTokens() {
 170  5
         return getRequiredTokens();
 171  
     }
 172  
 
 173  
     @Override
 174  
     public void beginTree(DetailAST rootAST) {
 175  
         // clear data
 176  7
         variableStack.clear();
 177  7
     }
 178  
 
 179  
     @Override
 180  
     public void visitToken(DetailAST ast) {
 181  128
         switch (ast.getType()) {
 182  
             case TokenTypes.OBJBLOCK:
 183  7
                 enterBlock();
 184  7
                 break;
 185  
             case TokenTypes.LITERAL_FOR:
 186  
             case TokenTypes.FOR_ITERATOR:
 187  
             case TokenTypes.FOR_EACH_CLAUSE:
 188  
                 //we need that Tokens only at leaveToken()
 189  48
                 break;
 190  
             case TokenTypes.ASSIGN:
 191  
             case TokenTypes.PLUS_ASSIGN:
 192  
             case TokenTypes.MINUS_ASSIGN:
 193  
             case TokenTypes.STAR_ASSIGN:
 194  
             case TokenTypes.DIV_ASSIGN:
 195  
             case TokenTypes.MOD_ASSIGN:
 196  
             case TokenTypes.SR_ASSIGN:
 197  
             case TokenTypes.BSR_ASSIGN:
 198  
             case TokenTypes.SL_ASSIGN:
 199  
             case TokenTypes.BAND_ASSIGN:
 200  
             case TokenTypes.BXOR_ASSIGN:
 201  
             case TokenTypes.BOR_ASSIGN:
 202  
             case TokenTypes.INC:
 203  
             case TokenTypes.POST_INC:
 204  
             case TokenTypes.DEC:
 205  
             case TokenTypes.POST_DEC:
 206  72
                 checkIdent(ast);
 207  72
                 break;
 208  
             default:
 209  1
                 throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast);
 210  
         }
 211  127
     }
 212  
 
 213  
     @Override
 214  
     public void leaveToken(DetailAST ast) {
 215  127
         switch (ast.getType()) {
 216  
             case TokenTypes.FOR_ITERATOR:
 217  17
                 leaveForIter(ast.getParent());
 218  17
                 break;
 219  
             case TokenTypes.FOR_EACH_CLAUSE:
 220  7
                 if (!skipEnhancedForLoopVariable) {
 221  4
                     final DetailAST paramDef = ast.findFirstToken(TokenTypes.VARIABLE_DEF);
 222  4
                     leaveForEach(paramDef);
 223  4
                 }
 224  
                 break;
 225  
             case TokenTypes.LITERAL_FOR:
 226  24
                 if (!getCurrentVariables().isEmpty()) {
 227  20
                     leaveForDef(ast);
 228  
                 }
 229  
                 break;
 230  
             case TokenTypes.OBJBLOCK:
 231  6
                 exitBlock();
 232  6
                 break;
 233  
             case TokenTypes.ASSIGN:
 234  
             case TokenTypes.PLUS_ASSIGN:
 235  
             case TokenTypes.MINUS_ASSIGN:
 236  
             case TokenTypes.STAR_ASSIGN:
 237  
             case TokenTypes.DIV_ASSIGN:
 238  
             case TokenTypes.MOD_ASSIGN:
 239  
             case TokenTypes.SR_ASSIGN:
 240  
             case TokenTypes.BSR_ASSIGN:
 241  
             case TokenTypes.SL_ASSIGN:
 242  
             case TokenTypes.BAND_ASSIGN:
 243  
             case TokenTypes.BXOR_ASSIGN:
 244  
             case TokenTypes.BOR_ASSIGN:
 245  
             case TokenTypes.INC:
 246  
             case TokenTypes.POST_INC:
 247  
             case TokenTypes.DEC:
 248  
             case TokenTypes.POST_DEC:
 249  
                 //we need that Tokens only at visitToken()
 250  72
                 break;
 251  
             default:
 252  1
                 throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast);
 253  
         }
 254  126
     }
 255  
 
 256  
     /**
 257  
      * Enters an inner class, which requires a new variable set.
 258  
      */
 259  
     private void enterBlock() {
 260  7
         variableStack.push(new ArrayDeque<>());
 261  7
     }
 262  
 
 263  
     /**
 264  
      * Leave an inner class, so restore variable set.
 265  
      */
 266  
     private void exitBlock() {
 267  6
         variableStack.pop();
 268  6
     }
 269  
 
 270  
     /**
 271  
      * Get current variable stack.
 272  
      * @return current variable stack
 273  
      */
 274  
     private Deque<String> getCurrentVariables() {
 275  151
         return variableStack.peek();
 276  
     }
 277  
 
 278  
     /**
 279  
      * Check if ident is parameter.
 280  
      * @param ast ident to check.
 281  
      */
 282  
     private void checkIdent(DetailAST ast) {
 283  72
         final Deque<String> currentVariables = getCurrentVariables();
 284  72
         if (currentVariables != null && !currentVariables.isEmpty()) {
 285  18
             final DetailAST identAST = ast.getFirstChild();
 286  
 
 287  18
             if (identAST != null && identAST.getType() == TokenTypes.IDENT
 288  13
                 && getCurrentVariables().contains(identAST.getText())) {
 289  20
                 log(ast.getLineNo(), ast.getColumnNo(),
 290  10
                     MSG_KEY, identAST.getText());
 291  
             }
 292  
         }
 293  72
     }
 294  
 
 295  
     /**
 296  
      * Push current variables to the stack.
 297  
      * @param ast a for definition.
 298  
      */
 299  
     private void leaveForIter(DetailAST ast) {
 300  17
         final Set<String> variablesToPutInScope = getVariablesManagedByForLoop(ast);
 301  17
         for (String variableName : variablesToPutInScope) {
 302  17
             getCurrentVariables().push(variableName);
 303  17
         }
 304  17
     }
 305  
 
 306  
     /**
 307  
      * Determines which variable are specific to for loop and should not be
 308  
      * change by inner loop body.
 309  
      * @param ast For Loop
 310  
      * @return Set of Variable Name which are managed by for
 311  
      */
 312  
     private static Set<String> getVariablesManagedByForLoop(DetailAST ast) {
 313  31
         final Set<String> initializedVariables = getForInitVariables(ast);
 314  31
         final Set<String> iteratingVariables = getForIteratorVariables(ast);
 315  62
         return initializedVariables.stream().filter(iteratingVariables::contains)
 316  31
             .collect(Collectors.toSet());
 317  
     }
 318  
 
 319  
     /**
 320  
      * Push current variables to the stack.
 321  
      * @param paramDef a for-each clause variable
 322  
      */
 323  
     private void leaveForEach(DetailAST paramDef) {
 324  4
         final DetailAST paramName = paramDef.findFirstToken(TokenTypes.IDENT);
 325  4
         getCurrentVariables().push(paramName.getText());
 326  4
     }
 327  
 
 328  
     /**
 329  
      * Pops the variables from the stack.
 330  
      * @param ast a for definition.
 331  
      */
 332  
     private void leaveForDef(DetailAST ast) {
 333  20
         final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT);
 334  20
         if (forInitAST == null) {
 335  6
             if (!skipEnhancedForLoopVariable) {
 336  
                 // this is for-each loop, just pop variables
 337  4
                 getCurrentVariables().pop();
 338  
             }
 339  
         }
 340  
         else {
 341  14
             final Set<String> variablesManagedByForLoop = getVariablesManagedByForLoop(ast);
 342  14
             popCurrentVariables(variablesManagedByForLoop.size());
 343  
         }
 344  20
     }
 345  
 
 346  
     /**
 347  
      * Pops given number of variables from currentVariables.
 348  
      * @param count Count of variables to be popped from currentVariables
 349  
      */
 350  
     private void popCurrentVariables(int count) {
 351  31
         for (int i = 0; i < count; i++) {
 352  17
             getCurrentVariables().pop();
 353  
         }
 354  14
     }
 355  
 
 356  
     /**
 357  
      * Get all variables initialized In init part of for loop.
 358  
      * @param ast for loop token
 359  
      * @return set of variables initialized in for loop
 360  
      */
 361  
     private static Set<String> getForInitVariables(DetailAST ast) {
 362  31
         final Set<String> initializedVariables = new HashSet<>();
 363  31
         final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT);
 364  
 
 365  31
         for (DetailAST parameterDefAST = forInitAST.findFirstToken(TokenTypes.VARIABLE_DEF);
 366  90
              parameterDefAST != null;
 367  59
              parameterDefAST = parameterDefAST.getNextSibling()) {
 368  59
             if (parameterDefAST.getType() == TokenTypes.VARIABLE_DEF) {
 369  45
                 final DetailAST param =
 370  45
                         parameterDefAST.findFirstToken(TokenTypes.IDENT);
 371  
 
 372  45
                 initializedVariables.add(param.getText());
 373  
             }
 374  
         }
 375  31
         return initializedVariables;
 376  
     }
 377  
 
 378  
     /**
 379  
      * Get all variables which for loop iterating part change in every loop.
 380  
      * @param ast for loop literal(TokenTypes.LITERAL_FOR)
 381  
      * @return names of variables change in iterating part of for
 382  
      */
 383  
     private static Set<String> getForIteratorVariables(DetailAST ast) {
 384  31
         final Set<String> iteratorVariables = new HashSet<>();
 385  31
         final DetailAST forIteratorAST = ast.findFirstToken(TokenTypes.FOR_ITERATOR);
 386  31
         final DetailAST forUpdateListAST = forIteratorAST.findFirstToken(TokenTypes.ELIST);
 387  
 
 388  31
         findChildrenOfExpressionType(forUpdateListAST).stream()
 389  31
             .filter(iteratingExpressionAST -> {
 390  39
                 return MUTATION_OPERATIONS.contains(iteratingExpressionAST.getType());
 391  31
             }).forEach(iteratingExpressionAST -> {
 392  38
                 final DetailAST oneVariableOperatorChild = iteratingExpressionAST.getFirstChild();
 393  38
                 if (oneVariableOperatorChild.getType() == TokenTypes.IDENT) {
 394  37
                     iteratorVariables.add(oneVariableOperatorChild.getText());
 395  
                 }
 396  38
             });
 397  
 
 398  31
         return iteratorVariables;
 399  
     }
 400  
 
 401  
     /**
 402  
      * Find all child of given AST of type TokenType.EXPR
 403  
      * @param ast parent of expressions to find
 404  
      * @return all child of given ast
 405  
      */
 406  
     private static List<DetailAST> findChildrenOfExpressionType(DetailAST ast) {
 407  31
         final List<DetailAST> foundExpressions = new LinkedList<>();
 408  31
         if (ast != null) {
 409  29
             for (DetailAST iteratingExpressionAST = ast.findFirstToken(TokenTypes.EXPR);
 410  78
                  iteratingExpressionAST != null;
 411  49
                  iteratingExpressionAST = iteratingExpressionAST.getNextSibling()) {
 412  49
                 if (iteratingExpressionAST.getType() == TokenTypes.EXPR) {
 413  39
                     foundExpressions.add(iteratingExpressionAST.getFirstChild());
 414  
                 }
 415  
             }
 416  
         }
 417  31
         return foundExpressions;
 418  
     }
 419  
 }