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