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