001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 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.Arrays;
023
024import antlr.collections.AST;
025import com.puppycrawl.tools.checkstyle.StatelessCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.TokenTypes;
029
030/**
031 * <p>
032 * Checks for assignments in subexpressions, such as in
033 * {@code String s = Integer.toString(i = 2);}.
034 * </p>
035 * <p>
036 * Rationale: With the exception of {@code for} iterators, all assignments
037 * should occur in their own top-level statement to increase readability.
038 * With inner assignments like the above it is difficult to see all places
039 * where a variable is set.
040 * </p>
041 *
042 */
043@StatelessCheck
044public class InnerAssignmentCheck
045        extends AbstractCheck {
046
047    /**
048     * A key is pointing to the warning message text in "messages.properties"
049     * file.
050     */
051    public static final String MSG_KEY = "assignment.inner.avoid";
052
053    /**
054     * List of allowed AST types from an assignment AST node
055     * towards the root.
056     */
057    private static final int[][] ALLOWED_ASSIGNMENT_CONTEXT = {
058        {TokenTypes.EXPR, TokenTypes.SLIST},
059        {TokenTypes.VARIABLE_DEF},
060        {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_INIT},
061        {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_ITERATOR},
062        {TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR}, {
063            TokenTypes.RESOURCE,
064            TokenTypes.RESOURCES,
065            TokenTypes.RESOURCE_SPECIFICATION,
066        },
067        {TokenTypes.EXPR, TokenTypes.LAMBDA},
068    };
069
070    /**
071     * List of allowed AST types from an assignment AST node
072     * towards the root.
073     */
074    private static final int[][] CONTROL_CONTEXT = {
075        {TokenTypes.EXPR, TokenTypes.LITERAL_DO},
076        {TokenTypes.EXPR, TokenTypes.LITERAL_FOR},
077        {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE},
078        {TokenTypes.EXPR, TokenTypes.LITERAL_IF},
079        {TokenTypes.EXPR, TokenTypes.LITERAL_ELSE},
080    };
081
082    /**
083     * List of allowed AST types from a comparison node (above an assignment)
084     * towards the root.
085     */
086    private static final int[][] ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT = {
087        {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE, },
088    };
089
090    /**
091     * The token types that identify comparison operators.
092     */
093    private static final int[] COMPARISON_TYPES = {
094        TokenTypes.EQUAL,
095        TokenTypes.GE,
096        TokenTypes.GT,
097        TokenTypes.LE,
098        TokenTypes.LT,
099        TokenTypes.NOT_EQUAL,
100    };
101
102    static {
103        Arrays.sort(COMPARISON_TYPES);
104    }
105
106    @Override
107    public int[] getDefaultTokens() {
108        return getRequiredTokens();
109    }
110
111    @Override
112    public int[] getAcceptableTokens() {
113        return getRequiredTokens();
114    }
115
116    @Override
117    public int[] getRequiredTokens() {
118        return new int[] {
119            TokenTypes.ASSIGN,            // '='
120            TokenTypes.DIV_ASSIGN,        // "/="
121            TokenTypes.PLUS_ASSIGN,       // "+="
122            TokenTypes.MINUS_ASSIGN,      //"-="
123            TokenTypes.STAR_ASSIGN,       // "*="
124            TokenTypes.MOD_ASSIGN,        // "%="
125            TokenTypes.SR_ASSIGN,         // ">>="
126            TokenTypes.BSR_ASSIGN,        // ">>>="
127            TokenTypes.SL_ASSIGN,         // "<<="
128            TokenTypes.BXOR_ASSIGN,       // "^="
129            TokenTypes.BOR_ASSIGN,        // "|="
130            TokenTypes.BAND_ASSIGN,       // "&="
131        };
132    }
133
134    @Override
135    public void visitToken(DetailAST ast) {
136        if (!isInContext(ast, ALLOWED_ASSIGNMENT_CONTEXT)
137                && !isInNoBraceControlStatement(ast)
138                && !isInWhileIdiom(ast)) {
139            log(ast, MSG_KEY);
140        }
141    }
142
143    /**
144     * Determines if ast is in the body of a flow control statement without
145     * braces. An example of such a statement would be
146     * <p>
147     * <pre>
148     * if (y < 0)
149     *     x = y;
150     * </pre>
151     * </p>
152     * <p>
153     * This leads to the following AST structure:
154     * </p>
155     * <p>
156     * <pre>
157     * LITERAL_IF
158     *     LPAREN
159     *     EXPR // test
160     *     RPAREN
161     *     EXPR // body
162     *     SEMI
163     * </pre>
164     * </p>
165     * <p>
166     * We need to ensure that ast is in the body and not in the test.
167     * </p>
168     *
169     * @param ast an assignment operator AST
170     * @return whether ast is in the body of a flow control statement
171     */
172    private static boolean isInNoBraceControlStatement(DetailAST ast) {
173        boolean result = false;
174        if (isInContext(ast, CONTROL_CONTEXT)) {
175            final DetailAST expr = ast.getParent();
176            final AST exprNext = expr.getNextSibling();
177            result = exprNext.getType() == TokenTypes.SEMI;
178        }
179        return result;
180    }
181
182    /**
183     * Tests whether the given AST is used in the "assignment in while" idiom.
184     * <pre>
185     * String line;
186     * while ((line = bufferedReader.readLine()) != null) {
187     *    // process the line
188     * }
189     * </pre>
190     * Assignment inside a condition is not a problem here, as the assignment is surrounded by an
191     * extra pair of parentheses. The comparison is {@code != null} and there is no chance that
192     * intention was to write {@code line == reader.readLine()}.
193     *
194     * @param ast assignment AST
195     * @return whether the context of the assignment AST indicates the idiom
196     */
197    private static boolean isInWhileIdiom(DetailAST ast) {
198        boolean result = false;
199        if (isComparison(ast.getParent())) {
200            result = isInContext(
201                    ast.getParent(), ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT);
202        }
203        return result;
204    }
205
206    /**
207     * Checks if an AST is a comparison operator.
208     * @param ast the AST to check
209     * @return true iff ast is a comparison operator.
210     */
211    private static boolean isComparison(DetailAST ast) {
212        final int astType = ast.getType();
213        return Arrays.binarySearch(COMPARISON_TYPES, astType) >= 0;
214    }
215
216    /**
217     * Tests whether the provided AST is in
218     * one of the given contexts.
219     *
220     * @param ast the AST from which to start walking towards root
221     * @param contextSet the contexts to test against.
222     *
223     * @return whether the parents nodes of ast match one of the allowed type paths.
224     */
225    private static boolean isInContext(DetailAST ast, int[]... contextSet) {
226        boolean found = false;
227        for (int[] element : contextSet) {
228            DetailAST current = ast;
229            for (int anElement : element) {
230                current = current.getParent();
231                if (current.getType() == anElement) {
232                    found = true;
233                }
234                else {
235                    found = false;
236                    break;
237                }
238            }
239
240            if (found) {
241                break;
242            }
243        }
244        return found;
245    }
246
247}