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