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     * 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}