001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2022 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 * <module name="InnerAssignment"/> 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<String> myList = new ArrayList<String>(); 087 * myList.add(nameOne = "tom"); // violation 088 * for (int k = 0; k < 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 * <pre> 244 * if (y < 0) 245 * x = y; 246 * </pre> 247 * <p> 248 * This leads to the following AST structure: 249 * </p> 250 * <pre> 251 * LITERAL_IF 252 * LPAREN 253 * EXPR // test 254 * RPAREN 255 * EXPR // body 256 * SEMI 257 * </pre> 258 * <p> 259 * We need to ensure that ast is in the body and not in the test. 260 * </p> 261 * 262 * @param ast an assignment operator AST 263 * @return whether ast is in the body of a flow control statement 264 */ 265 private static boolean isInNoBraceControlStatement(DetailAST ast) { 266 boolean result = false; 267 if (isInContext(ast, CONTROL_CONTEXT)) { 268 final DetailAST expr = ast.getParent(); 269 final DetailAST exprNext = expr.getNextSibling(); 270 result = exprNext.getType() == TokenTypes.SEMI; 271 } 272 return result; 273 } 274 275 /** 276 * Tests whether the given AST is used in the "assignment in loop" idiom. 277 * <pre> 278 * String line; 279 * while ((line = bufferedReader.readLine()) != null) { 280 * // process the line 281 * } 282 * for (;(line = bufferedReader.readLine()) != null;) { 283 * // process the line 284 * } 285 * do { 286 * // process the line 287 * } 288 * while ((line = bufferedReader.readLine()) != null); 289 * </pre> 290 * Assignment inside a condition is not a problem here, as the assignment is surrounded by an 291 * extra pair of parentheses. The comparison is {@code != null} and there is no chance that 292 * intention was to write {@code line == reader.readLine()}. 293 * 294 * @param ast assignment AST 295 * @return whether the context of the assignment AST indicates the idiom 296 */ 297 private static boolean isInLoopIdiom(DetailAST ast) { 298 boolean result = false; 299 if (isComparison(ast.getParent())) { 300 result = isInContext(ast.getParent(), 301 ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT, 302 LOOP_IDIOM_IGNORED_PARENTS 303 ); 304 } 305 return result; 306 } 307 308 /** 309 * Checks if an AST is a comparison operator. 310 * 311 * @param ast the AST to check 312 * @return true iff ast is a comparison operator. 313 */ 314 private static boolean isComparison(DetailAST ast) { 315 final int astType = ast.getType(); 316 return Arrays.binarySearch(COMPARISON_TYPES, astType) >= 0; 317 } 318 319 /** 320 * Tests whether the provided AST is in 321 * one of the given contexts. 322 * 323 * @param ast the AST from which to start walking towards root 324 * @param contextSet the contexts to test against. 325 * @param skipTokens parent token types to ignore 326 * 327 * @return whether the parents nodes of ast match one of the allowed type paths. 328 */ 329 private static boolean isInContext(DetailAST ast, int[][] contextSet, int... skipTokens) { 330 boolean found = false; 331 for (int[] element : contextSet) { 332 DetailAST current = ast; 333 for (int anElement : element) { 334 current = getParent(current, skipTokens); 335 if (current.getType() == anElement) { 336 found = true; 337 } 338 else { 339 found = false; 340 break; 341 } 342 } 343 344 if (found) { 345 break; 346 } 347 } 348 return found; 349 } 350 351 /** 352 * Get ast parent, ignoring token types from {@code skipTokens}. 353 * 354 * @param ast token to get parent 355 * @param skipTokens token types to skip 356 * @return first not ignored parent of ast 357 */ 358 private static DetailAST getParent(DetailAST ast, int... skipTokens) { 359 DetailAST result = ast.getParent(); 360 while (Arrays.binarySearch(skipTokens, result.getType()) > -1) { 361 result = result.getParent(); 362 } 363 return result; 364 } 365 366}