View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.BitSet;
23  
24  import com.puppycrawl.tools.checkstyle.StatelessCheck;
25  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
26  import com.puppycrawl.tools.checkstyle.api.DetailAST;
27  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
28  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
29  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
30  
31  /**
32   * <div>
33   * Checks for assignments in subexpressions, such as in
34   * {@code String s = Integer.toString(i = 2);}.
35   * </div>
36   *
37   * <p>
38   * Rationale: Except for the loop idioms,
39   * all assignments should occur in their own top-level statement to increase readability.
40   * With inner assignments like the one given above, it is difficult to see all places
41   * where a variable is set.
42   * </p>
43   *
44   * <p>
45   * Note: Check allows usage of the popular assignments in loops:
46   * </p>
47   * <pre>
48   * String line;
49   * while ((line = bufferedReader.readLine()) != null) { // OK
50   *   // process the line
51   * }
52   *
53   * for (;(line = bufferedReader.readLine()) != null;) { // OK
54   *   // process the line
55   * }
56   *
57   * do {
58   *   // process the line
59   * }
60   * while ((line = bufferedReader.readLine()) != null); // OK
61   * </pre>
62   *
63   * <p>
64   * Assignment inside a condition is not a problem here, as the assignment is surrounded
65   * by an extra pair of parentheses. The comparison is {@code != null} and there is no chance that
66   * intention was to write {@code line == reader.readLine()}.
67   * </p>
68   *
69   * <p>
70   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
71   * </p>
72   *
73   * <p>
74   * Violation Message Keys:
75   * </p>
76   * <ul>
77   * <li>
78   * {@code assignment.inner.avoid}
79   * </li>
80   * </ul>
81   *
82   * @since 3.0
83   */
84  @StatelessCheck
85  public class InnerAssignmentCheck
86          extends AbstractCheck {
87  
88      /**
89       * A key is pointing to the warning message text in "messages.properties"
90       * file.
91       */
92      public static final String MSG_KEY = "assignment.inner.avoid";
93  
94      /**
95       * Allowed AST types from an assignment AST node
96       * towards the root.
97       */
98      private static final int[][] ALLOWED_ASSIGNMENT_CONTEXT = {
99          {TokenTypes.EXPR, TokenTypes.SLIST},
100         {TokenTypes.VARIABLE_DEF},
101         {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_INIT},
102         {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_ITERATOR},
103         {TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR}, {
104             TokenTypes.RESOURCE,
105             TokenTypes.RESOURCES,
106             TokenTypes.RESOURCE_SPECIFICATION,
107         },
108         {TokenTypes.EXPR, TokenTypes.LAMBDA},
109         {TokenTypes.EXPR, TokenTypes.SWITCH_RULE, TokenTypes.LITERAL_SWITCH, TokenTypes.SLIST},
110     };
111 
112     /**
113      * Allowed AST types from an assignment AST node
114      * towards the root.
115      */
116     private static final int[][] CONTROL_CONTEXT = {
117         {TokenTypes.EXPR, TokenTypes.LITERAL_DO},
118         {TokenTypes.EXPR, TokenTypes.LITERAL_FOR},
119         {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE},
120         {TokenTypes.EXPR, TokenTypes.LITERAL_IF},
121         {TokenTypes.EXPR, TokenTypes.LITERAL_ELSE},
122     };
123 
124     /**
125      * Allowed AST types from a comparison node (above an assignment)
126      * towards the root.
127      */
128     private static final int[][] ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT = {
129         {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE},
130         {TokenTypes.EXPR, TokenTypes.FOR_CONDITION},
131         {TokenTypes.EXPR, TokenTypes.LITERAL_DO},
132     };
133 
134     /**
135      * The token types that identify comparison operators.
136      */
137     private static final BitSet COMPARISON_TYPES = TokenUtil.asBitSet(
138         TokenTypes.EQUAL,
139         TokenTypes.GE,
140         TokenTypes.GT,
141         TokenTypes.LE,
142         TokenTypes.LT,
143         TokenTypes.NOT_EQUAL
144     );
145 
146     /**
147      * The token types that are ignored while checking "loop-idiom".
148      */
149     private static final BitSet LOOP_IDIOM_IGNORED_PARENTS = TokenUtil.asBitSet(
150         TokenTypes.LAND,
151         TokenTypes.LOR,
152         TokenTypes.LNOT,
153         TokenTypes.BOR,
154         TokenTypes.BAND
155     );
156 
157     @Override
158     public int[] getDefaultTokens() {
159         return getRequiredTokens();
160     }
161 
162     @Override
163     public int[] getAcceptableTokens() {
164         return getRequiredTokens();
165     }
166 
167     @Override
168     public int[] getRequiredTokens() {
169         return new int[] {
170             TokenTypes.ASSIGN,            // '='
171             TokenTypes.DIV_ASSIGN,        // "/="
172             TokenTypes.PLUS_ASSIGN,       // "+="
173             TokenTypes.MINUS_ASSIGN,      // "-="
174             TokenTypes.STAR_ASSIGN,       // "*="
175             TokenTypes.MOD_ASSIGN,        // "%="
176             TokenTypes.SR_ASSIGN,         // ">>="
177             TokenTypes.BSR_ASSIGN,        // ">>>="
178             TokenTypes.SL_ASSIGN,         // "<<="
179             TokenTypes.BXOR_ASSIGN,       // "^="
180             TokenTypes.BOR_ASSIGN,        // "|="
181             TokenTypes.BAND_ASSIGN,       // "&="
182         };
183     }
184 
185     @Override
186     public void visitToken(DetailAST ast) {
187         if (!isInContext(ast, ALLOWED_ASSIGNMENT_CONTEXT, CommonUtil.EMPTY_BIT_SET)
188                 && !isInNoBraceControlStatement(ast)
189                 && !isInLoopIdiom(ast)) {
190             log(ast, MSG_KEY);
191         }
192     }
193 
194     /**
195      * Determines if ast is in the body of a flow control statement without
196      * braces. An example of such a statement would be
197      * <pre>
198      * if (y &lt; 0)
199      *     x = y;
200      * </pre>
201      *
202      * <p>
203      * This leads to the following AST structure:
204      * </p>
205      * <pre>
206      * LITERAL_IF
207      *     LPAREN
208      *     EXPR // test
209      *     RPAREN
210      *     EXPR // body
211      *     SEMI
212      * </pre>
213      *
214      * <p>
215      * We need to ensure that ast is in the body and not in the test.
216      * </p>
217      *
218      * @param ast an assignment operator AST
219      * @return whether ast is in the body of a flow control statement
220      */
221     private static boolean isInNoBraceControlStatement(DetailAST ast) {
222         boolean result = false;
223         if (isInContext(ast, CONTROL_CONTEXT, CommonUtil.EMPTY_BIT_SET)) {
224             final DetailAST expr = ast.getParent();
225             final DetailAST exprNext = expr.getNextSibling();
226             result = exprNext.getType() == TokenTypes.SEMI;
227         }
228         return result;
229     }
230 
231     /**
232      * Tests whether the given AST is used in the "assignment in loop" idiom.
233      * <pre>
234      * String line;
235      * while ((line = bufferedReader.readLine()) != null) {
236      *   // process the line
237      * }
238      * for (;(line = bufferedReader.readLine()) != null;) {
239      *   // process the line
240      * }
241      * do {
242      *   // process the line
243      * }
244      * while ((line = bufferedReader.readLine()) != null);
245      * </pre>
246      * Assignment inside a condition is not a problem here, as the assignment is surrounded by an
247      * extra pair of parentheses. The comparison is {@code != null} and there is no chance that
248      * intention was to write {@code line == reader.readLine()}.
249      *
250      * @param ast assignment AST
251      * @return whether the context of the assignment AST indicates the idiom
252      */
253     private static boolean isInLoopIdiom(DetailAST ast) {
254         return isComparison(ast.getParent())
255                     && isInContext(ast.getParent(),
256                             ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT,
257                             LOOP_IDIOM_IGNORED_PARENTS);
258     }
259 
260     /**
261      * Checks if an AST is a comparison operator.
262      *
263      * @param ast the AST to check
264      * @return true iff ast is a comparison operator.
265      */
266     private static boolean isComparison(DetailAST ast) {
267         final int astType = ast.getType();
268         return COMPARISON_TYPES.get(astType);
269     }
270 
271     /**
272      * Tests whether the provided AST is in
273      * one of the given contexts.
274      *
275      * @param ast the AST from which to start walking towards root
276      * @param contextSet the contexts to test against.
277      * @param skipTokens parent token types to ignore
278      *
279      * @return whether the parents nodes of ast match one of the allowed type paths.
280      */
281     private static boolean isInContext(DetailAST ast, int[][] contextSet, BitSet skipTokens) {
282         boolean found = false;
283         for (int[] element : contextSet) {
284             DetailAST current = ast;
285             for (int anElement : element) {
286                 current = getParent(current, skipTokens);
287                 if (current.getType() == anElement) {
288                     found = true;
289                 }
290                 else {
291                     found = false;
292                     break;
293                 }
294             }
295 
296             if (found) {
297                 break;
298             }
299         }
300         return found;
301     }
302 
303     /**
304      * Get ast parent, ignoring token types from {@code skipTokens}.
305      *
306      * @param ast token to get parent
307      * @param skipTokens token types to skip
308      * @return first not ignored parent of ast
309      */
310     private static DetailAST getParent(DetailAST ast, BitSet skipTokens) {
311         DetailAST result = ast.getParent();
312         while (skipTokens.get(result.getType())) {
313             result = result.getParent();
314         }
315         return result;
316     }
317 
318 }