001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2022 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 * <p>
039 * Checks that for loop control variables are not modified
040 * inside the for block. An example is:
041 * </p>
042 * <pre>
043 * for (int i = 0; i &lt; 1; i++) {
044 *   i++; // violation
045 * }
046 * </pre>
047 * <p>
048 * Rationale: If the control variable is modified inside the loop
049 * body, the program flow becomes more difficult to follow.
050 * See <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14">
051 * FOR statement</a> specification for more details.
052 * </p>
053 * <p>
054 * Such loop would be suppressed:
055 * </p>
056 * <pre>
057 * for (int i = 0; i &lt; 10;) {
058 *   i++;
059 * }
060 * </pre>
061 * <p>
062 * NOTE:The check works with only primitive type variables.
063 * The check will not work for arrays used as control variable.An example is
064 * </p>
065 * <pre>
066 * for (int a[]={0};a[0] &lt; 10;a[0]++) {
067 *  a[0]++;   // it will skip this violation
068 * }
069 * </pre>
070 * <ul>
071 * <li>
072 * Property {@code skipEnhancedForLoopVariable} - Control whether to check
073 * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14.2">
074 * enhanced for-loop</a> variable.
075 * Type is {@code boolean}.
076 * Default value is {@code false}.
077 * </li>
078 * </ul>
079 * <p>
080 * To configure the check:
081 * </p>
082 * <pre>
083 * &lt;module name="ModifiedControlVariable"/&gt;
084 * </pre>
085 * <p>
086 * Example:
087 * </p>
088 * <pre>
089 * for(int i=0;i &lt; 8;i++) {
090 *   i++; // violation, control variable modified
091 * }
092 * String args1[]={"Coding", "block"};
093 * for (String arg: args1) {
094 *   arg = arg.trim(); // violation, control variable modified
095 * }
096 * </pre>
097 * <p>
098 * By default, This Check validates
099 *  <a href = "https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14.2">
100 * Enhanced For-Loop</a>.
101 * </p>
102 * <p>
103 * Option 'skipEnhancedForLoopVariable' could be used to skip check of variable
104 *  from Enhanced For Loop.
105 * </p>
106 * <p>
107 * An example of how to configure the check so that it skips enhanced For Loop Variable is:
108 * </p>
109 * <pre>
110 * &lt;module name="ModifiedControlVariable"&gt;
111 *   &lt;property name="skipEnhancedForLoopVariable" value="true"/&gt;
112 * &lt;/module&gt;
113 * </pre>
114 * <p>Example:</p>
115 *
116 * <pre>
117 * for(int i=0;i &lt; 8;i++) {
118 *   i++; // violation, control variable modified
119 * }
120 * String args1[]={"Coding", "block"};
121 * for (String arg: args1) {
122 *   arg = arg.trim(); // ok
123 * }
124 * </pre>
125 * <p>
126 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
127 * </p>
128 * <p>
129 * Violation Message Keys:
130 * </p>
131 * <ul>
132 * <li>
133 * {@code modified.control.variable}
134 * </li>
135 * </ul>
136 *
137 * @since 3.5
138 */
139@FileStatefulCheck
140public final class ModifiedControlVariableCheck extends AbstractCheck {
141
142    /**
143     * A key is pointing to the warning message text in "messages.properties"
144     * file.
145     */
146    public static final String MSG_KEY = "modified.control.variable";
147
148    /**
149     * Message thrown with IllegalStateException.
150     */
151    private static final String ILLEGAL_TYPE_OF_TOKEN = "Illegal type of token: ";
152
153    /** Operations which can change control variable in update part of the loop. */
154    private static final BitSet MUTATION_OPERATIONS = TokenUtil.asBitSet(
155            TokenTypes.POST_INC,
156            TokenTypes.POST_DEC,
157            TokenTypes.DEC,
158            TokenTypes.INC,
159            TokenTypes.ASSIGN);
160
161    /** Stack of block parameters. */
162    private final Deque<Deque<String>> variableStack = new ArrayDeque<>();
163
164    /**
165     * Control whether to check
166     * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14.2">
167     * enhanced for-loop</a> variable.
168     */
169    private boolean skipEnhancedForLoopVariable;
170
171    /**
172     * Setter to control whether to check
173     * <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-14.html#jls-14.14.2">
174     * enhanced for-loop</a> variable.
175     *
176     * @param skipEnhancedForLoopVariable whether to skip enhanced for-loop variable
177     */
178    public void setSkipEnhancedForLoopVariable(boolean skipEnhancedForLoopVariable) {
179        this.skipEnhancedForLoopVariable = skipEnhancedForLoopVariable;
180    }
181
182    @Override
183    public int[] getDefaultTokens() {
184        return getRequiredTokens();
185    }
186
187    @Override
188    public int[] getRequiredTokens() {
189        return new int[] {
190            TokenTypes.OBJBLOCK,
191            TokenTypes.LITERAL_FOR,
192            TokenTypes.FOR_ITERATOR,
193            TokenTypes.FOR_EACH_CLAUSE,
194            TokenTypes.ASSIGN,
195            TokenTypes.PLUS_ASSIGN,
196            TokenTypes.MINUS_ASSIGN,
197            TokenTypes.STAR_ASSIGN,
198            TokenTypes.DIV_ASSIGN,
199            TokenTypes.MOD_ASSIGN,
200            TokenTypes.SR_ASSIGN,
201            TokenTypes.BSR_ASSIGN,
202            TokenTypes.SL_ASSIGN,
203            TokenTypes.BAND_ASSIGN,
204            TokenTypes.BXOR_ASSIGN,
205            TokenTypes.BOR_ASSIGN,
206            TokenTypes.INC,
207            TokenTypes.POST_INC,
208            TokenTypes.DEC,
209            TokenTypes.POST_DEC,
210        };
211    }
212
213    @Override
214    public int[] getAcceptableTokens() {
215        return getRequiredTokens();
216    }
217
218    @Override
219    public void beginTree(DetailAST rootAST) {
220        // clear data
221        variableStack.clear();
222    }
223
224    @Override
225    public void visitToken(DetailAST ast) {
226        switch (ast.getType()) {
227            case TokenTypes.OBJBLOCK:
228                enterBlock();
229                break;
230            case TokenTypes.LITERAL_FOR:
231            case TokenTypes.FOR_ITERATOR:
232            case TokenTypes.FOR_EACH_CLAUSE:
233                // we need that Tokens only at leaveToken()
234                break;
235            case TokenTypes.ASSIGN:
236            case TokenTypes.PLUS_ASSIGN:
237            case TokenTypes.MINUS_ASSIGN:
238            case TokenTypes.STAR_ASSIGN:
239            case TokenTypes.DIV_ASSIGN:
240            case TokenTypes.MOD_ASSIGN:
241            case TokenTypes.SR_ASSIGN:
242            case TokenTypes.BSR_ASSIGN:
243            case TokenTypes.SL_ASSIGN:
244            case TokenTypes.BAND_ASSIGN:
245            case TokenTypes.BXOR_ASSIGN:
246            case TokenTypes.BOR_ASSIGN:
247            case TokenTypes.INC:
248            case TokenTypes.POST_INC:
249            case TokenTypes.DEC:
250            case TokenTypes.POST_DEC:
251                checkIdent(ast);
252                break;
253            default:
254                throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast);
255        }
256    }
257
258    @Override
259    public void leaveToken(DetailAST ast) {
260        switch (ast.getType()) {
261            case TokenTypes.FOR_ITERATOR:
262                leaveForIter(ast.getParent());
263                break;
264            case TokenTypes.FOR_EACH_CLAUSE:
265                if (!skipEnhancedForLoopVariable) {
266                    final DetailAST paramDef = ast.findFirstToken(TokenTypes.VARIABLE_DEF);
267                    leaveForEach(paramDef);
268                }
269                break;
270            case TokenTypes.LITERAL_FOR:
271                leaveForDef(ast);
272                break;
273            case TokenTypes.OBJBLOCK:
274                exitBlock();
275                break;
276            case TokenTypes.ASSIGN:
277            case TokenTypes.PLUS_ASSIGN:
278            case TokenTypes.MINUS_ASSIGN:
279            case TokenTypes.STAR_ASSIGN:
280            case TokenTypes.DIV_ASSIGN:
281            case TokenTypes.MOD_ASSIGN:
282            case TokenTypes.SR_ASSIGN:
283            case TokenTypes.BSR_ASSIGN:
284            case TokenTypes.SL_ASSIGN:
285            case TokenTypes.BAND_ASSIGN:
286            case TokenTypes.BXOR_ASSIGN:
287            case TokenTypes.BOR_ASSIGN:
288            case TokenTypes.INC:
289            case TokenTypes.POST_INC:
290            case TokenTypes.DEC:
291            case TokenTypes.POST_DEC:
292                // we need that Tokens only at visitToken()
293                break;
294            default:
295                throw new IllegalStateException(ILLEGAL_TYPE_OF_TOKEN + ast);
296        }
297    }
298
299    /**
300     * Enters an inner class, which requires a new variable set.
301     */
302    private void enterBlock() {
303        variableStack.push(new ArrayDeque<>());
304    }
305
306    /**
307     * Leave an inner class, so restore variable set.
308     */
309    private void exitBlock() {
310        variableStack.pop();
311    }
312
313    /**
314     * Get current variable stack.
315     *
316     * @return current variable stack
317     */
318    private Deque<String> getCurrentVariables() {
319        return variableStack.peek();
320    }
321
322    /**
323     * Check if ident is parameter.
324     *
325     * @param ast ident to check.
326     */
327    private void checkIdent(DetailAST ast) {
328        final Deque<String> currentVariables = getCurrentVariables();
329        final DetailAST identAST = ast.getFirstChild();
330
331        if (identAST != null && identAST.getType() == TokenTypes.IDENT
332            && currentVariables.contains(identAST.getText())) {
333            log(ast, MSG_KEY, identAST.getText());
334        }
335    }
336
337    /**
338     * Push current variables to the stack.
339     *
340     * @param ast a for definition.
341     */
342    private void leaveForIter(DetailAST ast) {
343        final Set<String> variablesToPutInScope = getVariablesManagedByForLoop(ast);
344        for (String variableName : variablesToPutInScope) {
345            getCurrentVariables().push(variableName);
346        }
347    }
348
349    /**
350     * Determines which variable are specific to for loop and should not be
351     * change by inner loop body.
352     *
353     * @param ast For Loop
354     * @return Set of Variable Name which are managed by for
355     */
356    private static Set<String> getVariablesManagedByForLoop(DetailAST ast) {
357        final Set<String> initializedVariables = getForInitVariables(ast);
358        final Set<String> iteratingVariables = getForIteratorVariables(ast);
359        return initializedVariables.stream().filter(iteratingVariables::contains)
360            .collect(Collectors.toSet());
361    }
362
363    /**
364     * Push current variables to the stack.
365     *
366     * @param paramDef a for-each clause variable
367     */
368    private void leaveForEach(DetailAST paramDef) {
369        final DetailAST paramName = paramDef.findFirstToken(TokenTypes.IDENT);
370        getCurrentVariables().push(paramName.getText());
371    }
372
373    /**
374     * Pops the variables from the stack.
375     *
376     * @param ast a for definition.
377     */
378    private void leaveForDef(DetailAST ast) {
379        final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT);
380        if (forInitAST == null) {
381            if (!skipEnhancedForLoopVariable) {
382                // this is for-each loop, just pop variables
383                getCurrentVariables().pop();
384            }
385        }
386        else {
387            final Set<String> variablesManagedByForLoop = getVariablesManagedByForLoop(ast);
388            popCurrentVariables(variablesManagedByForLoop.size());
389        }
390    }
391
392    /**
393     * Pops given number of variables from currentVariables.
394     *
395     * @param count Count of variables to be popped from currentVariables
396     */
397    private void popCurrentVariables(int count) {
398        for (int i = 0; i < count; i++) {
399            getCurrentVariables().pop();
400        }
401    }
402
403    /**
404     * Get all variables initialized In init part of for loop.
405     *
406     * @param ast for loop token
407     * @return set of variables initialized in for loop
408     */
409    private static Set<String> getForInitVariables(DetailAST ast) {
410        final Set<String> initializedVariables = new HashSet<>();
411        final DetailAST forInitAST = ast.findFirstToken(TokenTypes.FOR_INIT);
412
413        for (DetailAST parameterDefAST = forInitAST.findFirstToken(TokenTypes.VARIABLE_DEF);
414             parameterDefAST != null;
415             parameterDefAST = parameterDefAST.getNextSibling()) {
416            if (parameterDefAST.getType() == TokenTypes.VARIABLE_DEF) {
417                final DetailAST param =
418                        parameterDefAST.findFirstToken(TokenTypes.IDENT);
419
420                initializedVariables.add(param.getText());
421            }
422        }
423        return initializedVariables;
424    }
425
426    /**
427     * Get all variables which for loop iterating part change in every loop.
428     *
429     * @param ast for loop literal(TokenTypes.LITERAL_FOR)
430     * @return names of variables change in iterating part of for
431     */
432    private static Set<String> getForIteratorVariables(DetailAST ast) {
433        final Set<String> iteratorVariables = new HashSet<>();
434        final DetailAST forIteratorAST = ast.findFirstToken(TokenTypes.FOR_ITERATOR);
435        final DetailAST forUpdateListAST = forIteratorAST.findFirstToken(TokenTypes.ELIST);
436
437        findChildrenOfExpressionType(forUpdateListAST).stream()
438            .filter(iteratingExpressionAST -> {
439                return MUTATION_OPERATIONS.get(iteratingExpressionAST.getType());
440            }).forEach(iteratingExpressionAST -> {
441                final DetailAST oneVariableOperatorChild = iteratingExpressionAST.getFirstChild();
442                iteratorVariables.add(oneVariableOperatorChild.getText());
443            });
444
445        return iteratorVariables;
446    }
447
448    /**
449     * Find all child of given AST of type TokenType.EXPR
450     *
451     * @param ast parent of expressions to find
452     * @return all child of given ast
453     */
454    private static List<DetailAST> findChildrenOfExpressionType(DetailAST ast) {
455        final List<DetailAST> foundExpressions = new LinkedList<>();
456        if (ast != null) {
457            for (DetailAST iteratingExpressionAST = ast.findFirstToken(TokenTypes.EXPR);
458                 iteratingExpressionAST != null;
459                 iteratingExpressionAST = iteratingExpressionAST.getNextSibling()) {
460                if (iteratingExpressionAST.getType() == TokenTypes.EXPR) {
461                    foundExpressions.add(iteratingExpressionAST.getFirstChild());
462                }
463            }
464        }
465        return foundExpressions;
466    }
467
468}