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