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