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 {@code for} iterators and assignment in {@code while} idiom, 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 assignment in {@code while} idiom: 042 * </p> 043 * <pre> 044 * String line; 045 * while ((line = bufferedReader.readLine()) != null) { 046 * // process the line 047 * } 048 * </pre> 049 * <p> 050 * Assignment inside a condition is not a problem here, as the assignment is surrounded 051 * by an extra pair of parentheses. The comparison is {@code != null} and there is no chance that 052 * intention was to write {@code line == reader.readLine()}. 053 * </p> 054 * <p> 055 * To configure the check: 056 * </p> 057 * <pre> 058 * <module name="InnerAssignment"/> 059 * </pre> 060 * <p>Example:</p> 061 * <pre> 062 * class MyClass { 063 * 064 * void foo() { 065 * int a, b; 066 * a = b = 5; // violation, assignment to each variable should be in a separate statement 067 * a = b += 5; // violation 068 * 069 * a = 5; // OK 070 * b = 5; // OK 071 * a = 5; b = 5; // OK 072 * 073 * double myDouble; 074 * double[] doubleArray = new double[] {myDouble = 4.5, 15.5}; // violation 075 * 076 * String nameOne; 077 * List<String> myList = new ArrayList<String>(); 078 * myList.add(nameOne = "tom"); // violation 079 * for (int k = 0; k < 10; k = k + 2) { // OK 080 * // some code 081 * } 082 * 083 * boolean someVal; 084 * if (someVal = true) { // violation 085 * // some code 086 * } 087 * 088 * while (someVal = false) {} // violation 089 * 090 * InputStream is = new FileInputStream("textFile.txt"); 091 * while ((b = is.read()) != -1) { // OK, this is a common idiom 092 * // some code 093 * } 094 * 095 * } 096 * 097 * boolean testMethod() { 098 * boolean val; 099 * return val = true; // violation 100 * } 101 * } 102 * </pre> 103 * 104 * @since 3.0 105 */ 106@StatelessCheck 107public class InnerAssignmentCheck 108 extends AbstractCheck { 109 110 /** 111 * A key is pointing to the warning message text in "messages.properties" 112 * file. 113 */ 114 public static final String MSG_KEY = "assignment.inner.avoid"; 115 116 /** 117 * List of allowed AST types from an assignment AST node 118 * towards the root. 119 */ 120 private static final int[][] ALLOWED_ASSIGNMENT_CONTEXT = { 121 {TokenTypes.EXPR, TokenTypes.SLIST}, 122 {TokenTypes.VARIABLE_DEF}, 123 {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_INIT}, 124 {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_ITERATOR}, 125 {TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR}, { 126 TokenTypes.RESOURCE, 127 TokenTypes.RESOURCES, 128 TokenTypes.RESOURCE_SPECIFICATION, 129 }, 130 {TokenTypes.EXPR, TokenTypes.LAMBDA}, 131 }; 132 133 /** 134 * List of allowed AST types from an assignment AST node 135 * towards the root. 136 */ 137 private static final int[][] CONTROL_CONTEXT = { 138 {TokenTypes.EXPR, TokenTypes.LITERAL_DO}, 139 {TokenTypes.EXPR, TokenTypes.LITERAL_FOR}, 140 {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE}, 141 {TokenTypes.EXPR, TokenTypes.LITERAL_IF}, 142 {TokenTypes.EXPR, TokenTypes.LITERAL_ELSE}, 143 }; 144 145 /** 146 * List of allowed AST types from a comparison node (above an assignment) 147 * towards the root. 148 */ 149 private static final int[][] ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT = { 150 {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE, }, 151 }; 152 153 /** 154 * The token types that identify comparison operators. 155 */ 156 private static final int[] COMPARISON_TYPES = { 157 TokenTypes.EQUAL, 158 TokenTypes.GE, 159 TokenTypes.GT, 160 TokenTypes.LE, 161 TokenTypes.LT, 162 TokenTypes.NOT_EQUAL, 163 }; 164 165 /** 166 * The token types that are ignored while checking "while-idiom". 167 */ 168 private static final int[] WHILE_IDIOM_IGNORED_PARENTS = { 169 TokenTypes.LAND, 170 TokenTypes.LOR, 171 TokenTypes.LNOT, 172 TokenTypes.BOR, 173 TokenTypes.BAND, 174 }; 175 176 static { 177 Arrays.sort(COMPARISON_TYPES); 178 Arrays.sort(WHILE_IDIOM_IGNORED_PARENTS); 179 } 180 181 @Override 182 public int[] getDefaultTokens() { 183 return getRequiredTokens(); 184 } 185 186 @Override 187 public int[] getAcceptableTokens() { 188 return getRequiredTokens(); 189 } 190 191 @Override 192 public int[] getRequiredTokens() { 193 return new int[] { 194 TokenTypes.ASSIGN, // '=' 195 TokenTypes.DIV_ASSIGN, // "/=" 196 TokenTypes.PLUS_ASSIGN, // "+=" 197 TokenTypes.MINUS_ASSIGN, // "-=" 198 TokenTypes.STAR_ASSIGN, // "*=" 199 TokenTypes.MOD_ASSIGN, // "%=" 200 TokenTypes.SR_ASSIGN, // ">>=" 201 TokenTypes.BSR_ASSIGN, // ">>>=" 202 TokenTypes.SL_ASSIGN, // "<<=" 203 TokenTypes.BXOR_ASSIGN, // "^=" 204 TokenTypes.BOR_ASSIGN, // "|=" 205 TokenTypes.BAND_ASSIGN, // "&=" 206 }; 207 } 208 209 @Override 210 public void visitToken(DetailAST ast) { 211 if (!isInContext(ast, ALLOWED_ASSIGNMENT_CONTEXT) 212 && !isInNoBraceControlStatement(ast) 213 && !isInWhileIdiom(ast)) { 214 log(ast, MSG_KEY); 215 } 216 } 217 218 /** 219 * Determines if ast is in the body of a flow control statement without 220 * braces. An example of such a statement would be 221 * <p> 222 * <pre> 223 * if (y < 0) 224 * x = y; 225 * </pre> 226 * </p> 227 * <p> 228 * This leads to the following AST structure: 229 * </p> 230 * <p> 231 * <pre> 232 * LITERAL_IF 233 * LPAREN 234 * EXPR // test 235 * RPAREN 236 * EXPR // body 237 * SEMI 238 * </pre> 239 * </p> 240 * <p> 241 * We need to ensure that ast is in the body and not in the test. 242 * </p> 243 * 244 * @param ast an assignment operator AST 245 * @return whether ast is in the body of a flow control statement 246 */ 247 private static boolean isInNoBraceControlStatement(DetailAST ast) { 248 boolean result = false; 249 if (isInContext(ast, CONTROL_CONTEXT)) { 250 final DetailAST expr = ast.getParent(); 251 final DetailAST exprNext = expr.getNextSibling(); 252 result = exprNext.getType() == TokenTypes.SEMI; 253 } 254 return result; 255 } 256 257 /** 258 * Tests whether the given AST is used in the "assignment in while" idiom. 259 * <pre> 260 * String line; 261 * while ((line = bufferedReader.readLine()) != null) { 262 * // process the line 263 * } 264 * </pre> 265 * Assignment inside a condition is not a problem here, as the assignment is surrounded by an 266 * extra pair of parentheses. The comparison is {@code != null} and there is no chance that 267 * intention was to write {@code line == reader.readLine()}. 268 * 269 * @param ast assignment AST 270 * @return whether the context of the assignment AST indicates the idiom 271 */ 272 private static boolean isInWhileIdiom(DetailAST ast) { 273 boolean result = false; 274 if (isComparison(ast.getParent())) { 275 result = isInContext(ast.getParent(), 276 ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT, 277 WHILE_IDIOM_IGNORED_PARENTS 278 ); 279 } 280 return result; 281 } 282 283 /** 284 * Checks if an AST is a comparison operator. 285 * 286 * @param ast the AST to check 287 * @return true iff ast is a comparison operator. 288 */ 289 private static boolean isComparison(DetailAST ast) { 290 final int astType = ast.getType(); 291 return Arrays.binarySearch(COMPARISON_TYPES, astType) >= 0; 292 } 293 294 /** 295 * Tests whether the provided AST is in 296 * one of the given contexts. 297 * 298 * @param ast the AST from which to start walking towards root 299 * @param contextSet the contexts to test against. 300 * @param skipTokens parent token types to ignore 301 * 302 * @return whether the parents nodes of ast match one of the allowed type paths. 303 */ 304 private static boolean isInContext(DetailAST ast, int[][] contextSet, int... skipTokens) { 305 boolean found = false; 306 for (int[] element : contextSet) { 307 DetailAST current = ast; 308 for (int anElement : element) { 309 current = getParent(current, skipTokens); 310 if (current.getType() == anElement) { 311 found = true; 312 } 313 else { 314 found = false; 315 break; 316 } 317 } 318 319 if (found) { 320 break; 321 } 322 } 323 return found; 324 } 325 326 /** 327 * Get ast parent, ignoring token types from {@code skipTokens}. 328 * 329 * @param ast token to get parent 330 * @param skipTokens token types to skip 331 * @return first not ignored parent of ast 332 */ 333 private static DetailAST getParent(DetailAST ast, int... skipTokens) { 334 DetailAST result = ast.getParent(); 335 while (Arrays.binarySearch(skipTokens, result.getType()) > -1) { 336 result = result.getParent(); 337 } 338 return result; 339 } 340 341}