001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 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;
025
026import com.puppycrawl.tools.checkstyle.api.Check;
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 * @author lkuehne
043 */
044public class InnerAssignmentCheck
045        extends Check {
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 getAcceptableTokens();
109    }
110
111    @Override
112    public int[] getAcceptableTokens() {
113        return new int[] {
114            TokenTypes.ASSIGN,            // '='
115            TokenTypes.DIV_ASSIGN,        // "/="
116            TokenTypes.PLUS_ASSIGN,       // "+="
117            TokenTypes.MINUS_ASSIGN,      //"-="
118            TokenTypes.STAR_ASSIGN,       // "*="
119            TokenTypes.MOD_ASSIGN,        // "%="
120            TokenTypes.SR_ASSIGN,         // ">>="
121            TokenTypes.BSR_ASSIGN,        // ">>>="
122            TokenTypes.SL_ASSIGN,         // "<<="
123            TokenTypes.BXOR_ASSIGN,       // "^="
124            TokenTypes.BOR_ASSIGN,        // "|="
125            TokenTypes.BAND_ASSIGN,       // "&="
126        };
127    }
128
129    @Override
130    public int[] getRequiredTokens() {
131        return getAcceptableTokens();
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.getLineNo(), ast.getColumnNo(), 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        if (!isInContext(ast, CONTROL_CONTEXT)) {
174            return false;
175        }
176        final DetailAST expr = ast.getParent();
177        final AST exprNext = expr.getNextSibling();
178        return exprNext.getType() == TokenTypes.SEMI;
179    }
180
181    /**
182     * Tests whether the given AST is used in the "assignment in while" idiom.
183     * <pre>
184     * String line;
185     * while ((line = bufferedReader.readLine()) != null) {
186     *    // process the line
187     * }
188     * </pre>
189     * Assignment inside a condition is not a problem here, as the assignment is surrounded by an
190     * extra pair of parentheses. The comparison is {@code != null} and there is no chance that
191     * intention was to write {@code line == reader.readLine()}.
192     *
193     * @param ast assignment AST
194     * @return whether the context of the assignment AST indicates the idiom
195     */
196    private static boolean isInWhileIdiom(DetailAST ast) {
197        if (!isComparison(ast.getParent())) {
198            return false;
199        }
200        return isInContext(
201                ast.getParent(), ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT);
202    }
203
204    /**
205     * Checks if an AST is a comparison operator.
206     * @param ast the AST to check
207     * @return true iff ast is a comparison operator.
208     */
209    private static boolean isComparison(DetailAST ast) {
210        final int astType = ast.getType();
211        return Arrays.binarySearch(COMPARISON_TYPES, astType) >= 0;
212    }
213
214    /**
215     * Tests whether the provided AST is in
216     * one of the given contexts.
217     *
218     * @param ast the AST from which to start walking towards root
219     * @param contextSet the contexts to test against.
220     *
221     * @return whether the parents nodes of ast match one of the allowed type paths.
222     */
223    private static boolean isInContext(DetailAST ast, int[]... contextSet) {
224        boolean found = false;
225        for (int[] element : contextSet) {
226            DetailAST current = ast;
227            for (int anElement : element) {
228                current = current.getParent();
229                if (current.getType() == anElement) {
230                    found = true;
231                }
232                else {
233                    found = false;
234                    break;
235                }
236            }
237
238            if (found) {
239                break;
240            }
241        }
242        return found;
243    }
244}