001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.BitSet;
024import java.util.Deque;
025import java.util.HashSet;
026import java.util.LinkedList;
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:
166                enterBlock();
167                break;
168            case TokenTypes.LITERAL_FOR:
169            case TokenTypes.FOR_ITERATOR:
170            case TokenTypes.FOR_EACH_CLAUSE:
171                // we need that Tokens only at leaveToken()
172                break;
173            case TokenTypes.ASSIGN:
174            case TokenTypes.PLUS_ASSIGN:
175            case TokenTypes.MINUS_ASSIGN:
176            case TokenTypes.STAR_ASSIGN:
177            case TokenTypes.DIV_ASSIGN:
178            case TokenTypes.MOD_ASSIGN:
179            case TokenTypes.SR_ASSIGN:
180            case TokenTypes.BSR_ASSIGN:
181            case TokenTypes.SL_ASSIGN:
182            case TokenTypes.BAND_ASSIGN:
183            case TokenTypes.BXOR_ASSIGN:
184            case TokenTypes.BOR_ASSIGN:
185            case TokenTypes.INC:
186            case TokenTypes.POST_INC:
187            case TokenTypes.DEC:
188            case TokenTypes.POST_DEC:
189                checkIdent(ast);
190                break;
191            default:
192                throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast);
193        }
194    }
195
196    @Override
197    public void leaveToken(DetailAST ast) {
198        switch (ast.getType()) {
199            case TokenTypes.FOR_ITERATOR:
200                leaveForIter(ast.getParent());
201                break;
202            case TokenTypes.FOR_EACH_CLAUSE:
203                if (!skipEnhancedForLoopVariable) {
204                    final DetailAST paramDef = ast.findFirstToken(TokenTypes.VARIABLE_DEF);
205                    leaveForEach(paramDef);
206                }
207                break;
208            case TokenTypes.LITERAL_FOR:
209                leaveForDef(ast);
210                break;
211            case TokenTypes.OBJBLOCK:
212                exitBlock();
213                break;
214            case TokenTypes.ASSIGN:
215            case TokenTypes.PLUS_ASSIGN:
216            case TokenTypes.MINUS_ASSIGN:
217            case TokenTypes.STAR_ASSIGN:
218            case TokenTypes.DIV_ASSIGN:
219            case TokenTypes.MOD_ASSIGN:
220            case TokenTypes.SR_ASSIGN:
221            case TokenTypes.BSR_ASSIGN:
222            case TokenTypes.SL_ASSIGN:
223            case TokenTypes.BAND_ASSIGN:
224            case TokenTypes.BXOR_ASSIGN:
225            case TokenTypes.BOR_ASSIGN:
226            case TokenTypes.INC:
227            case TokenTypes.POST_INC:
228            case TokenTypes.DEC:
229            case TokenTypes.POST_DEC:
230                // we need that Tokens only at visitToken()
231                break;
232            default:
233                throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast);
234        }
235    }
236
237    /**
238     * Enters an inner class, which requires a new variable set.
239     */
240    private void enterBlock() {
241        variableStack.push(new ArrayDeque<>());
242    }
243
244    /**
245     * Leave an inner class, so restore variable set.
246     */
247    private void exitBlock() {
248        variableStack.pop();
249    }
250
251    /**
252     * Get current variable stack.
253     *
254     * @return current variable stack
255     */
256    private Deque<String> getCurrentVariables() {
257        return variableStack.peek();
258    }
259
260    /**
261     * Check if ident is parameter.
262     *
263     * @param ast ident to check.
264     */
265    private void checkIdent(DetailAST ast) {
266        final Deque<String> currentVariables = getCurrentVariables();
267        final DetailAST identAST = ast.getFirstChild();
268
269        if (identAST != null && identAST.getType() == TokenTypes.IDENT
270            && currentVariables.contains(identAST.getText())) {
271            log(ast, MSG_KEY, identAST.getText());
272        }
273    }
274
275    /**
276     * Push current variables to the stack.
277     *
278     * @param ast a for definition.
279     */
280    private void leaveForIter(DetailAST ast) {
281        final Set<String> variablesToPutInScope = getVariablesManagedByForLoop(ast);
282        for (String variableName : variablesToPutInScope) {
283            getCurrentVariables().push(variableName);
284        }
285    }
286
287    /**
288     * Determines which variable are specific to for loop and should not be
289     * change by inner loop body.
290     *
291     * @param ast For Loop
292     * @return Set of Variable Name which are managed by for
293     */
294    private static Set<String> getVariablesManagedByForLoop(DetailAST ast) {
295        final Set<String> initializedVariables = getForInitVariables(ast);
296        final Set<String> iteratingVariables = getForIteratorVariables(ast);
297        return initializedVariables.stream().filter(iteratingVariables::contains)
298            .collect(Collectors.toUnmodifiableSet());
299    }
300
301    /**
302     * Push current variables to the stack.
303     *
304     * @param paramDef a for-each clause variable
305     */
306    private void leaveForEach(DetailAST paramDef) {
307        // When using record decomposition in enhanced for loops,
308        // we are not able to declare a 'control variable'.
309        final boolean isRecordPattern = paramDef == null;
310
311        if (!isRecordPattern) {
312            final DetailAST paramName = paramDef.findFirstToken(TokenTypes.IDENT);
313            getCurrentVariables().push(paramName.getText());
314        }
315    }
316
317    /**
318     * Pops the variables from the stack.
319     *
320     * @param ast a for definition.
321     */
322    private void leaveForDef(DetailAST ast) {
323        final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT);
324        if (forInitAST == null) {
325            final Deque<String> currentVariables = getCurrentVariables();
326            if (!skipEnhancedForLoopVariable && !currentVariables.isEmpty()) {
327                // this is for-each loop, just pop variables
328                currentVariables.pop();
329            }
330        }
331        else {
332            final Set<String> variablesManagedByForLoop = getVariablesManagedByForLoop(ast);
333            popCurrentVariables(variablesManagedByForLoop.size());
334        }
335    }
336
337    /**
338     * Pops given number of variables from currentVariables.
339     *
340     * @param count Count of variables to be popped from currentVariables
341     */
342    private void popCurrentVariables(int count) {
343        for (int i = 0; i < count; i++) {
344            getCurrentVariables().pop();
345        }
346    }
347
348    /**
349     * Get all variables initialized In init part of for loop.
350     *
351     * @param ast for loop token
352     * @return set of variables initialized in for loop
353     */
354    private static Set<String> getForInitVariables(DetailAST ast) {
355        final Set<String> initializedVariables = new HashSet<>();
356        final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT);
357
358        for (DetailAST parameterDefAST = forInitAST.findFirstToken(TokenTypes.VARIABLE_DEF);
359             parameterDefAST != null;
360             parameterDefAST = parameterDefAST.getNextSibling()) {
361            if (parameterDefAST.getType() == TokenTypes.VARIABLE_DEF) {
362                final DetailAST param =
363                        parameterDefAST.findFirstToken(TokenTypes.IDENT);
364
365                initializedVariables.add(param.getText());
366            }
367        }
368        return initializedVariables;
369    }
370
371    /**
372     * Get all variables which for loop iterating part change in every loop.
373     *
374     * @param ast for loop literal(TokenTypes.LITERAL_FOR)
375     * @return names of variables change in iterating part of for
376     */
377    private static Set<String> getForIteratorVariables(DetailAST ast) {
378        final Set<String> iteratorVariables = new HashSet<>();
379        final DetailAST forIteratorAST = ast.findFirstToken(TokenTypes.FOR_ITERATOR);
380        final DetailAST forUpdateListAST = forIteratorAST.findFirstToken(TokenTypes.ELIST);
381
382        findChildrenOfExpressionType(forUpdateListAST).stream()
383            .filter(iteratingExpressionAST -> {
384                return MUTATION_OPERATIONS.get(iteratingExpressionAST.getType());
385            }).forEach(iteratingExpressionAST -> {
386                final DetailAST oneVariableOperatorChild = iteratingExpressionAST.getFirstChild();
387                iteratorVariables.add(oneVariableOperatorChild.getText());
388            });
389
390        return iteratorVariables;
391    }
392
393    /**
394     * Find all child of given AST of type TokenType.EXPR.
395     *
396     * @param ast parent of expressions to find
397     * @return all child of given ast
398     */
399    private static List<DetailAST> findChildrenOfExpressionType(DetailAST ast) {
400        final List<DetailAST> foundExpressions = new LinkedList<>();
401        if (ast != null) {
402            for (DetailAST iteratingExpressionAST = ast.findFirstToken(TokenTypes.EXPR);
403                 iteratingExpressionAST != null;
404                 iteratingExpressionAST = iteratingExpressionAST.getNextSibling()) {
405                if (iteratingExpressionAST.getType() == TokenTypes.EXPR) {
406                    foundExpressions.add(iteratingExpressionAST.getFirstChild());
407                }
408            }
409        }
410        return foundExpressions;
411    }
412
413}