001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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 com.puppycrawl.tools.checkstyle.StatelessCheck;
025import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailAST;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028
029/**
030 * <p>
031 * Checks for assignments in subexpressions, such as in
032 * {@code String s = Integer.toString(i = 2);}.
033 * </p>
034 * <p>
035 * Rationale: With the exception of loop idioms,
036 * all assignments should occur in their own top-level statement to increase readability.
037 * With inner assignments like the one given above, it is difficult to see all places
038 * where a variable is set.
039 * </p>
040 * <p>
041 * Note: Check allows usage of the popular assignments in loops:
042 * </p>
043 * <pre>
044 * String line;
045 * while ((line = bufferedReader.readLine()) != null) { // OK
046 *   // process the line
047 * }
048 *
049 * for (;(line = bufferedReader.readLine()) != null;) { // OK
050 *   // process the line
051 * }
052 *
053 * do {
054 *   // process the line
055 * }
056 * while ((line = bufferedReader.readLine()) != null); // OK
057 * </pre>
058 * <p>
059 * Assignment inside a condition is not a problem here, as the assignment is surrounded
060 * by an extra pair of parentheses. The comparison is {@code != null} and there is no chance that
061 * intention was to write {@code line == reader.readLine()}.
062 * </p>
063 * <p>
064 * To configure the check:
065 * </p>
066 * <pre>
067 * &lt;module name=&quot;InnerAssignment"/&gt;
068 * </pre>
069 * <p>Example:</p>
070 * <pre>
071 * class MyClass {
072 *
073 *   void foo() {
074 *     int a, b;
075 *     a = b = 5; // violation, assignment to each variable should be in a separate statement
076 *     a = b += 5; // violation
077 *
078 *     a = 5; // OK
079 *     b = 5; // OK
080 *     a = 5; b = 5; // OK
081 *
082 *     double myDouble;
083 *     double[] doubleArray = new double[] {myDouble = 4.5, 15.5}; // violation
084 *
085 *     String nameOne;
086 *     List&lt;String&gt; myList = new ArrayList&lt;String&gt;();
087 *     myList.add(nameOne = "tom"); // violation
088 *     for (int k = 0; k &lt; 10; k = k + 2) { // OK
089 *       // some code
090 *     }
091 *
092 *     boolean someVal;
093 *     if (someVal = true) { // violation
094 *       // some code
095 *     }
096 *
097 *     while (someVal = false) {} // violation
098 *
099 *     InputStream is = new FileInputStream("textFile.txt");
100 *     while ((b = is.read()) != -1) { // OK, this is a common idiom
101 *       // some code
102 *     }
103 *
104 *   }
105 *
106 *   boolean testMethod() {
107 *     boolean val;
108 *     return val = true; // violation
109 *   }
110 * }
111 * </pre>
112 * <p>
113 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
114 * </p>
115 * <p>
116 * Violation Message Keys:
117 * </p>
118 * <ul>
119 * <li>
120 * {@code assignment.inner.avoid}
121 * </li>
122 * </ul>
123 *
124 * @since 3.0
125 */
126@StatelessCheck
127public class InnerAssignmentCheck
128        extends AbstractCheck {
129
130    /**
131     * A key is pointing to the warning message text in "messages.properties"
132     * file.
133     */
134    public static final String MSG_KEY = "assignment.inner.avoid";
135
136    /**
137     * List of allowed AST types from an assignment AST node
138     * towards the root.
139     */
140    private static final int[][] ALLOWED_ASSIGNMENT_CONTEXT = {
141        {TokenTypes.EXPR, TokenTypes.SLIST},
142        {TokenTypes.VARIABLE_DEF},
143        {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_INIT},
144        {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_ITERATOR},
145        {TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR}, {
146            TokenTypes.RESOURCE,
147            TokenTypes.RESOURCES,
148            TokenTypes.RESOURCE_SPECIFICATION,
149        },
150        {TokenTypes.EXPR, TokenTypes.LAMBDA},
151    };
152
153    /**
154     * List of allowed AST types from an assignment AST node
155     * towards the root.
156     */
157    private static final int[][] CONTROL_CONTEXT = {
158        {TokenTypes.EXPR, TokenTypes.LITERAL_DO},
159        {TokenTypes.EXPR, TokenTypes.LITERAL_FOR},
160        {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE},
161        {TokenTypes.EXPR, TokenTypes.LITERAL_IF},
162        {TokenTypes.EXPR, TokenTypes.LITERAL_ELSE},
163    };
164
165    /**
166     * List of allowed AST types from a comparison node (above an assignment)
167     * towards the root.
168     */
169    private static final int[][] ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT = {
170        {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE},
171        {TokenTypes.EXPR, TokenTypes.FOR_CONDITION},
172        {TokenTypes.EXPR, TokenTypes.LITERAL_DO},
173    };
174
175    /**
176     * The token types that identify comparison operators.
177     */
178    private static final int[] COMPARISON_TYPES = {
179        TokenTypes.EQUAL,
180        TokenTypes.GE,
181        TokenTypes.GT,
182        TokenTypes.LE,
183        TokenTypes.LT,
184        TokenTypes.NOT_EQUAL,
185    };
186
187    /**
188     * The token types that are ignored while checking "loop-idiom".
189     */
190    private static final int[] LOOP_IDIOM_IGNORED_PARENTS = {
191        TokenTypes.LAND,
192        TokenTypes.LOR,
193        TokenTypes.LNOT,
194        TokenTypes.BOR,
195        TokenTypes.BAND,
196    };
197
198    static {
199        Arrays.sort(COMPARISON_TYPES);
200        Arrays.sort(LOOP_IDIOM_IGNORED_PARENTS);
201    }
202
203    @Override
204    public int[] getDefaultTokens() {
205        return getRequiredTokens();
206    }
207
208    @Override
209    public int[] getAcceptableTokens() {
210        return getRequiredTokens();
211    }
212
213    @Override
214    public int[] getRequiredTokens() {
215        return new int[] {
216            TokenTypes.ASSIGN,            // '='
217            TokenTypes.DIV_ASSIGN,        // "/="
218            TokenTypes.PLUS_ASSIGN,       // "+="
219            TokenTypes.MINUS_ASSIGN,      // "-="
220            TokenTypes.STAR_ASSIGN,       // "*="
221            TokenTypes.MOD_ASSIGN,        // "%="
222            TokenTypes.SR_ASSIGN,         // ">>="
223            TokenTypes.BSR_ASSIGN,        // ">>>="
224            TokenTypes.SL_ASSIGN,         // "<<="
225            TokenTypes.BXOR_ASSIGN,       // "^="
226            TokenTypes.BOR_ASSIGN,        // "|="
227            TokenTypes.BAND_ASSIGN,       // "&="
228        };
229    }
230
231    @Override
232    public void visitToken(DetailAST ast) {
233        if (!isInContext(ast, ALLOWED_ASSIGNMENT_CONTEXT)
234                && !isInNoBraceControlStatement(ast)
235                && !isInLoopIdiom(ast)) {
236            log(ast, MSG_KEY);
237        }
238    }
239
240    /**
241     * Determines if ast is in the body of a flow control statement without
242     * braces. An example of such a statement would be
243     * <p>
244     * <pre>
245     * if (y < 0)
246     *     x = y;
247     * </pre>
248     * </p>
249     * <p>
250     * This leads to the following AST structure:
251     * </p>
252     * <p>
253     * <pre>
254     * LITERAL_IF
255     *     LPAREN
256     *     EXPR // test
257     *     RPAREN
258     *     EXPR // body
259     *     SEMI
260     * </pre>
261     * </p>
262     * <p>
263     * We need to ensure that ast is in the body and not in the test.
264     * </p>
265     *
266     * @param ast an assignment operator AST
267     * @return whether ast is in the body of a flow control statement
268     */
269    private static boolean isInNoBraceControlStatement(DetailAST ast) {
270        boolean result = false;
271        if (isInContext(ast, CONTROL_CONTEXT)) {
272            final DetailAST expr = ast.getParent();
273            final DetailAST exprNext = expr.getNextSibling();
274            result = exprNext.getType() == TokenTypes.SEMI;
275        }
276        return result;
277    }
278
279    /**
280     * Tests whether the given AST is used in the "assignment in loop" idiom.
281     * <pre>
282     * String line;
283     * while ((line = bufferedReader.readLine()) != null) {
284     *   // process the line
285     * }
286     * for (;(line = bufferedReader.readLine()) != null;) {
287     *   // process the line
288     * }
289     * do {
290     *   // process the line
291     * }
292     * while ((line = bufferedReader.readLine()) != null);
293     * </pre>
294     * Assignment inside a condition is not a problem here, as the assignment is surrounded by an
295     * extra pair of parentheses. The comparison is {@code != null} and there is no chance that
296     * intention was to write {@code line == reader.readLine()}.
297     *
298     * @param ast assignment AST
299     * @return whether the context of the assignment AST indicates the idiom
300     */
301    private static boolean isInLoopIdiom(DetailAST ast) {
302        boolean result = false;
303        if (isComparison(ast.getParent())) {
304            result = isInContext(ast.getParent(),
305                ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT,
306                LOOP_IDIOM_IGNORED_PARENTS
307            );
308        }
309        return result;
310    }
311
312    /**
313     * Checks if an AST is a comparison operator.
314     *
315     * @param ast the AST to check
316     * @return true iff ast is a comparison operator.
317     */
318    private static boolean isComparison(DetailAST ast) {
319        final int astType = ast.getType();
320        return Arrays.binarySearch(COMPARISON_TYPES, astType) >= 0;
321    }
322
323    /**
324     * Tests whether the provided AST is in
325     * one of the given contexts.
326     *
327     * @param ast the AST from which to start walking towards root
328     * @param contextSet the contexts to test against.
329     * @param skipTokens parent token types to ignore
330     *
331     * @return whether the parents nodes of ast match one of the allowed type paths.
332     */
333    private static boolean isInContext(DetailAST ast, int[][] contextSet, int... skipTokens) {
334        boolean found = false;
335        for (int[] element : contextSet) {
336            DetailAST current = ast;
337            for (int anElement : element) {
338                current = getParent(current, skipTokens);
339                if (current.getType() == anElement) {
340                    found = true;
341                }
342                else {
343                    found = false;
344                    break;
345                }
346            }
347
348            if (found) {
349                break;
350            }
351        }
352        return found;
353    }
354
355    /**
356     * Get ast parent, ignoring token types from {@code skipTokens}.
357     *
358     * @param ast token to get parent
359     * @param skipTokens token types to skip
360     * @return first not ignored parent of ast
361     */
362    private static DetailAST getParent(DetailAST ast, int... skipTokens) {
363        DetailAST result = ast.getParent();
364        while (Arrays.binarySearch(skipTokens, result.getType()) > -1) {
365            result = result.getParent();
366        }
367        return result;
368    }
369
370}