View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2018 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.coding;
21  
22  import java.util.Arrays;
23  
24  import antlr.collections.AST;
25  import com.puppycrawl.tools.checkstyle.StatelessCheck;
26  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
27  import com.puppycrawl.tools.checkstyle.api.DetailAST;
28  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
29  
30  /**
31   * <p>
32   * Checks for assignments in subexpressions, such as in
33   * {@code String s = Integer.toString(i = 2);}.
34   * </p>
35   * <p>
36   * Rationale: With the exception of {@code for} iterators, all assignments
37   * should occur in their own top-level statement to increase readability.
38   * With inner assignments like the above it is difficult to see all places
39   * where a variable is set.
40   * </p>
41   *
42   */
43  @StatelessCheck
44  public class InnerAssignmentCheck
45          extends AbstractCheck {
46  
47      /**
48       * A key is pointing to the warning message text in "messages.properties"
49       * file.
50       */
51      public static final String MSG_KEY = "assignment.inner.avoid";
52  
53      /**
54       * List of allowed AST types from an assignment AST node
55       * towards the root.
56       */
57      private static final int[][] ALLOWED_ASSIGNMENT_CONTEXT = {
58          {TokenTypes.EXPR, TokenTypes.SLIST},
59          {TokenTypes.VARIABLE_DEF},
60          {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_INIT},
61          {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_ITERATOR},
62          {TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR}, {
63              TokenTypes.RESOURCE,
64              TokenTypes.RESOURCES,
65              TokenTypes.RESOURCE_SPECIFICATION,
66          },
67          {TokenTypes.EXPR, TokenTypes.LAMBDA},
68      };
69  
70      /**
71       * List of allowed AST types from an assignment AST node
72       * towards the root.
73       */
74      private static final int[][] CONTROL_CONTEXT = {
75          {TokenTypes.EXPR, TokenTypes.LITERAL_DO},
76          {TokenTypes.EXPR, TokenTypes.LITERAL_FOR},
77          {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE},
78          {TokenTypes.EXPR, TokenTypes.LITERAL_IF},
79          {TokenTypes.EXPR, TokenTypes.LITERAL_ELSE},
80      };
81  
82      /**
83       * List of allowed AST types from a comparison node (above an assignment)
84       * towards the root.
85       */
86      private static final int[][] ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT = {
87          {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE, },
88      };
89  
90      /**
91       * The token types that identify comparison operators.
92       */
93      private static final int[] COMPARISON_TYPES = {
94          TokenTypes.EQUAL,
95          TokenTypes.GE,
96          TokenTypes.GT,
97          TokenTypes.LE,
98          TokenTypes.LT,
99          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.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         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 }