001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.coding;
021
022import java.util.ArrayDeque;
023import java.util.ArrayList;
024import java.util.BitSet;
025import java.util.Deque;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Set;
029import java.util.stream.Collectors;
030
031import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
032import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
033import com.puppycrawl.tools.checkstyle.api.DetailAST;
034import com.puppycrawl.tools.checkstyle.api.TokenTypes;
035import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
036
037/**
038 * <div>
039 * Checks that for loop control variables are not modified
040 * inside the for block. An example is:
041 * </div>
042 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
043 * for (int i = 0; i &lt; 1; i++) {
044 *   i++; // violation
045 * }
046 * </code></pre></div>
047 *
048 * <p>
049 * Rationale: If the control variable is modified inside the loop
050 * body, the program flow becomes more difficult to follow.
051 * See <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14">
052 * FOR statement</a> specification for more details.
053 * </p>
054 *
055 * <p>
056 * Such loop would be suppressed:
057 * </p>
058 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
059 * for (int i = 0; i &lt; 10;) {
060 *   i++;
061 * }
062 * </code></pre></div>
063 *
064 * <p>
065 * NOTE:The check works with only primitive type variables.
066 * The check will not work for arrays used as control variable. An example is
067 * </p>
068 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
069 * for (int a[]={0};a[0] &lt; 10;a[0]++) {
070 *  a[0]++;   // it will skip this violation
071 * }
072 * </code></pre></div>
073 *
074 * @since 3.5
075 */
076@FileStatefulCheck
077public final class ModifiedControlVariableCheck extends AbstractCheck {
078
079    /**
080     * A key is pointing to the warning message text in "messages.properties"
081     * file.
082     */
083    public static final String MSG_KEY = "modified.control.variable";
084
085    /**
086     * Message thrown with IllegalStateException.
087     */
088    private static final String ILLEGAL_TYPE_OF_TOKEN = "Illegal type of token: ";
089
090    /** Operations which can change control variable in update part of the loop. */
091    private static final BitSet MUTATION_OPERATIONS = TokenUtil.asBitSet(
092            TokenTypes.POST_INC,
093            TokenTypes.POST_DEC,
094            TokenTypes.DEC,
095            TokenTypes.INC,
096            TokenTypes.ASSIGN);
097
098    /** Stack of block parameters. */
099    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}