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