View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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.ArrayList;
24  import java.util.BitSet;
25  import java.util.Deque;
26  import java.util.HashSet;
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 -> enterBlock();
166             case TokenTypes.LITERAL_FOR,
167                  TokenTypes.FOR_ITERATOR,
168                  TokenTypes.FOR_EACH_CLAUSE -> {
169                 // we need that Tokens only at leaveToken()
170             }
171             case TokenTypes.ASSIGN,
172                  TokenTypes.PLUS_ASSIGN,
173                  TokenTypes.MINUS_ASSIGN,
174                  TokenTypes.STAR_ASSIGN,
175                  TokenTypes.DIV_ASSIGN,
176                  TokenTypes.MOD_ASSIGN,
177                  TokenTypes.SR_ASSIGN,
178                  TokenTypes.BSR_ASSIGN,
179                  TokenTypes.SL_ASSIGN,
180                  TokenTypes.BAND_ASSIGN,
181                  TokenTypes.BXOR_ASSIGN,
182                  TokenTypes.BOR_ASSIGN,
183                  TokenTypes.INC,
184                  TokenTypes.POST_INC,
185                  TokenTypes.DEC,
186                  TokenTypes.POST_DEC ->
187                 checkIdent(ast);
188             default -> throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast);
189         }
190     }
191 
192     @Override
193     public void leaveToken(DetailAST ast) {
194         switch (ast.getType()) {
195             case TokenTypes.FOR_ITERATOR -> leaveForIter(ast.getParent());
196             case TokenTypes.FOR_EACH_CLAUSE -> {
197                 if (!skipEnhancedForLoopVariable) {
198                     final DetailAST paramDef = ast.findFirstToken(TokenTypes.VARIABLE_DEF);
199                     leaveForEach(paramDef);
200                 }
201             }
202             case TokenTypes.LITERAL_FOR -> leaveForDef(ast);
203             case TokenTypes.OBJBLOCK -> exitBlock();
204             case TokenTypes.ASSIGN,
205                  TokenTypes.PLUS_ASSIGN,
206                  TokenTypes.MINUS_ASSIGN,
207                  TokenTypes.STAR_ASSIGN,
208                  TokenTypes.DIV_ASSIGN,
209                  TokenTypes.MOD_ASSIGN,
210                  TokenTypes.SR_ASSIGN,
211                  TokenTypes.BSR_ASSIGN,
212                  TokenTypes.SL_ASSIGN,
213                  TokenTypes.BAND_ASSIGN,
214                  TokenTypes.BXOR_ASSIGN,
215                  TokenTypes.BOR_ASSIGN,
216                  TokenTypes.INC,
217                  TokenTypes.POST_INC,
218                  TokenTypes.DEC,
219                  TokenTypes.POST_DEC -> {
220                 // we need that Tokens only at visitToken()
221             }
222             default -> throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast);
223         }
224     }
225 
226     /**
227      * Enters an inner class, which requires a new variable set.
228      */
229     private void enterBlock() {
230         variableStack.push(new ArrayDeque<>());
231     }
232 
233     /**
234      * Leave an inner class, so restore variable set.
235      */
236     private void exitBlock() {
237         variableStack.pop();
238     }
239 
240     /**
241      * Get current variable stack.
242      *
243      * @return current variable stack
244      */
245     private Deque<String> getCurrentVariables() {
246         return variableStack.peek();
247     }
248 
249     /**
250      * Check if ident is parameter.
251      *
252      * @param ast ident to check.
253      */
254     private void checkIdent(DetailAST ast) {
255         final Deque<String> currentVariables = getCurrentVariables();
256         final DetailAST identAST = ast.getFirstChild();
257 
258         if (identAST != null && identAST.getType() == TokenTypes.IDENT
259             && currentVariables.contains(identAST.getText())) {
260             log(ast, MSG_KEY, identAST.getText());
261         }
262     }
263 
264     /**
265      * Push current variables to the stack.
266      *
267      * @param ast a for definition.
268      */
269     private void leaveForIter(DetailAST ast) {
270         final Set<String> variablesToPutInScope = getVariablesManagedByForLoop(ast);
271         for (String variableName : variablesToPutInScope) {
272             getCurrentVariables().push(variableName);
273         }
274     }
275 
276     /**
277      * Determines which variable are specific to for loop and should not be
278      * change by inner loop body.
279      *
280      * @param ast For Loop
281      * @return Set of Variable Name which are managed by for
282      */
283     private static Set<String> getVariablesManagedByForLoop(DetailAST ast) {
284         final Set<String> initializedVariables = getForInitVariables(ast);
285         final Set<String> iteratingVariables = getForIteratorVariables(ast);
286         return initializedVariables.stream().filter(iteratingVariables::contains)
287             .collect(Collectors.toUnmodifiableSet());
288     }
289 
290     /**
291      * Push current variables to the stack.
292      *
293      * @param paramDef a for-each clause variable
294      */
295     private void leaveForEach(DetailAST paramDef) {
296         // When using record decomposition in enhanced for loops,
297         // we are not able to declare a 'control variable'.
298         final boolean isRecordPattern = paramDef == null;
299 
300         if (!isRecordPattern) {
301             final DetailAST paramName = paramDef.findFirstToken(TokenTypes.IDENT);
302             getCurrentVariables().push(paramName.getText());
303         }
304     }
305 
306     /**
307      * Pops the variables from the stack.
308      *
309      * @param ast a for definition.
310      */
311     private void leaveForDef(DetailAST ast) {
312         final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT);
313         if (forInitAST == null) {
314             final Deque<String> currentVariables = getCurrentVariables();
315             if (!skipEnhancedForLoopVariable && !currentVariables.isEmpty()) {
316                 // this is for-each loop, just pop variables
317                 currentVariables.pop();
318             }
319         }
320         else {
321             final Set<String> variablesManagedByForLoop = getVariablesManagedByForLoop(ast);
322             popCurrentVariables(variablesManagedByForLoop.size());
323         }
324     }
325 
326     /**
327      * Pops given number of variables from currentVariables.
328      *
329      * @param count Count of variables to be popped from currentVariables
330      */
331     private void popCurrentVariables(int count) {
332         for (int i = 0; i < count; i++) {
333             getCurrentVariables().pop();
334         }
335     }
336 
337     /**
338      * Get all variables initialized In init part of for loop.
339      *
340      * @param ast for loop token
341      * @return set of variables initialized in for loop
342      */
343     private static Set<String> getForInitVariables(DetailAST ast) {
344         final Set<String> initializedVariables = new HashSet<>();
345         final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT);
346 
347         for (DetailAST parameterDefAST = forInitAST.findFirstToken(TokenTypes.VARIABLE_DEF);
348              parameterDefAST != null;
349              parameterDefAST = parameterDefAST.getNextSibling()) {
350             if (parameterDefAST.getType() == TokenTypes.VARIABLE_DEF) {
351                 final DetailAST param =
352                         parameterDefAST.findFirstToken(TokenTypes.IDENT);
353 
354                 initializedVariables.add(param.getText());
355             }
356         }
357         return initializedVariables;
358     }
359 
360     /**
361      * Get all variables which for loop iterating part change in every loop.
362      *
363      * @param ast for loop literal(TokenTypes.LITERAL_FOR)
364      * @return names of variables change in iterating part of for
365      */
366     private static Set<String> getForIteratorVariables(DetailAST ast) {
367         final Set<String> iteratorVariables = new HashSet<>();
368         final DetailAST forIteratorAST = ast.findFirstToken(TokenTypes.FOR_ITERATOR);
369         final DetailAST forUpdateListAST = forIteratorAST.findFirstToken(TokenTypes.ELIST);
370 
371         findChildrenOfExpressionType(forUpdateListAST).stream()
372             .filter(iteratingExpressionAST -> {
373                 return MUTATION_OPERATIONS.get(iteratingExpressionAST.getType());
374             }).forEach(iteratingExpressionAST -> {
375                 final DetailAST oneVariableOperatorChild = iteratingExpressionAST.getFirstChild();
376                 iteratorVariables.add(oneVariableOperatorChild.getText());
377             });
378 
379         return iteratorVariables;
380     }
381 
382     /**
383      * Find all child of given AST of type TokenType.EXPR.
384      *
385      * @param ast parent of expressions to find
386      * @return all child of given ast
387      */
388     private static List<DetailAST> findChildrenOfExpressionType(DetailAST ast) {
389         final List<DetailAST> foundExpressions = new ArrayList<>();
390         if (ast != null) {
391             for (DetailAST iteratingExpressionAST = ast.findFirstToken(TokenTypes.EXPR);
392                  iteratingExpressionAST != null;
393                  iteratingExpressionAST = iteratingExpressionAST.getNextSibling()) {
394                 if (iteratingExpressionAST.getType() == TokenTypes.EXPR) {
395                     foundExpressions.add(iteratingExpressionAST.getFirstChild());
396                 }
397             }
398         }
399         return foundExpressions;
400     }
401 
402 }