View Javadoc
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 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     private static final Set<Integer> MUTATION_OPERATIONS =
115         Arrays.stream(new Integer[] {
116             TokenTypes.POST_INC,
117             TokenTypes.POST_DEC,
118             TokenTypes.DEC,
119             TokenTypes.INC,
120             TokenTypes.ASSIGN,
121         }).collect(Collectors.toSet());
122 
123     /** Stack of block parameters. */
124     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         this.skipEnhancedForLoopVariable = skipEnhancedForLoopVariable;
135     }
136 
137     @Override
138     public int[] getDefaultTokens() {
139         return getRequiredTokens();
140     }
141 
142     @Override
143     public int[] getRequiredTokens() {
144         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         return getRequiredTokens();
171     }
172 
173     @Override
174     public void beginTree(DetailAST rootAST) {
175         // clear data
176         variableStack.clear();
177     }
178 
179     @Override
180     public void visitToken(DetailAST ast) {
181         switch (ast.getType()) {
182             case TokenTypes.OBJBLOCK:
183                 enterBlock();
184                 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                 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                 checkIdent(ast);
207                 break;
208             default:
209                 throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast);
210         }
211     }
212 
213     @Override
214     public void leaveToken(DetailAST ast) {
215         switch (ast.getType()) {
216             case TokenTypes.FOR_ITERATOR:
217                 leaveForIter(ast.getParent());
218                 break;
219             case TokenTypes.FOR_EACH_CLAUSE:
220                 if (!skipEnhancedForLoopVariable) {
221                     final DetailAST paramDef = ast.findFirstToken(TokenTypes.VARIABLE_DEF);
222                     leaveForEach(paramDef);
223                 }
224                 break;
225             case TokenTypes.LITERAL_FOR:
226                 if (!getCurrentVariables().isEmpty()) {
227                     leaveForDef(ast);
228                 }
229                 break;
230             case TokenTypes.OBJBLOCK:
231                 exitBlock();
232                 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                 break;
251             default:
252                 throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast);
253         }
254     }
255 
256     /**
257      * Enters an inner class, which requires a new variable set.
258      */
259     private void enterBlock() {
260         variableStack.push(new ArrayDeque<>());
261     }
262 
263     /**
264      * Leave an inner class, so restore variable set.
265      */
266     private void exitBlock() {
267         variableStack.pop();
268     }
269 
270     /**
271      * Get current variable stack.
272      * @return current variable stack
273      */
274     private Deque<String> getCurrentVariables() {
275         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         final Deque<String> currentVariables = getCurrentVariables();
284         if (currentVariables != null && !currentVariables.isEmpty()) {
285             final DetailAST identAST = ast.getFirstChild();
286 
287             if (identAST != null && identAST.getType() == TokenTypes.IDENT
288                 && getCurrentVariables().contains(identAST.getText())) {
289                 log(ast.getLineNo(), ast.getColumnNo(),
290                     MSG_KEY, identAST.getText());
291             }
292         }
293     }
294 
295     /**
296      * Push current variables to the stack.
297      * @param ast a for definition.
298      */
299     private void leaveForIter(DetailAST ast) {
300         final Set<String> variablesToPutInScope = getVariablesManagedByForLoop(ast);
301         for (String variableName : variablesToPutInScope) {
302             getCurrentVariables().push(variableName);
303         }
304     }
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         final Set<String> initializedVariables = getForInitVariables(ast);
314         final Set<String> iteratingVariables = getForIteratorVariables(ast);
315         return initializedVariables.stream().filter(iteratingVariables::contains)
316             .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         final DetailAST paramName = paramDef.findFirstToken(TokenTypes.IDENT);
325         getCurrentVariables().push(paramName.getText());
326     }
327 
328     /**
329      * Pops the variables from the stack.
330      * @param ast a for definition.
331      */
332     private void leaveForDef(DetailAST ast) {
333         final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT);
334         if (forInitAST == null) {
335             if (!skipEnhancedForLoopVariable) {
336                 // this is for-each loop, just pop variables
337                 getCurrentVariables().pop();
338             }
339         }
340         else {
341             final Set<String> variablesManagedByForLoop = getVariablesManagedByForLoop(ast);
342             popCurrentVariables(variablesManagedByForLoop.size());
343         }
344     }
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         for (int i = 0; i < count; i++) {
352             getCurrentVariables().pop();
353         }
354     }
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         final Set<String> initializedVariables = new HashSet<>();
363         final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT);
364 
365         for (DetailAST parameterDefAST = forInitAST.findFirstToken(TokenTypes.VARIABLE_DEF);
366              parameterDefAST != null;
367              parameterDefAST = parameterDefAST.getNextSibling()) {
368             if (parameterDefAST.getType() == TokenTypes.VARIABLE_DEF) {
369                 final DetailAST param =
370                         parameterDefAST.findFirstToken(TokenTypes.IDENT);
371 
372                 initializedVariables.add(param.getText());
373             }
374         }
375         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         final Set<String> iteratorVariables = new HashSet<>();
385         final DetailAST forIteratorAST = ast.findFirstToken(TokenTypes.FOR_ITERATOR);
386         final DetailAST forUpdateListAST = forIteratorAST.findFirstToken(TokenTypes.ELIST);
387 
388         findChildrenOfExpressionType(forUpdateListAST).stream()
389             .filter(iteratingExpressionAST -> {
390                 return MUTATION_OPERATIONS.contains(iteratingExpressionAST.getType());
391             }).forEach(iteratingExpressionAST -> {
392                 final DetailAST oneVariableOperatorChild = iteratingExpressionAST.getFirstChild();
393                 if (oneVariableOperatorChild.getType() == TokenTypes.IDENT) {
394                     iteratorVariables.add(oneVariableOperatorChild.getText());
395                 }
396             });
397 
398         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         final List<DetailAST> foundExpressions = new LinkedList<>();
408         if (ast != null) {
409             for (DetailAST iteratingExpressionAST = ast.findFirstToken(TokenTypes.EXPR);
410                  iteratingExpressionAST != null;
411                  iteratingExpressionAST = iteratingExpressionAST.getNextSibling()) {
412                 if (iteratingExpressionAST.getType() == TokenTypes.EXPR) {
413                     foundExpressions.add(iteratingExpressionAST.getFirstChild());
414                 }
415             }
416         }
417         return foundExpressions;
418     }
419 }