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