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