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 < 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 }