001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2024 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.BitSet; 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; 028import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 029import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 030 031/** 032 * <div> 033 * Checks for assignments in subexpressions, such as in 034 * {@code String s = Integer.toString(i = 2);}. 035 * </div> 036 * 037 * <p> 038 * Rationale: Except for the loop idioms, 039 * all assignments should occur in their own top-level statement to increase readability. 040 * With inner assignments like the one given above, it is difficult to see all places 041 * where a variable is set. 042 * </p> 043 * 044 * <p> 045 * Note: Check allows usage of the popular assignments in loops: 046 * </p> 047 * <pre> 048 * String line; 049 * while ((line = bufferedReader.readLine()) != null) { // OK 050 * // process the line 051 * } 052 * 053 * for (;(line = bufferedReader.readLine()) != null;) { // OK 054 * // process the line 055 * } 056 * 057 * do { 058 * // process the line 059 * } 060 * while ((line = bufferedReader.readLine()) != null); // OK 061 * </pre> 062 * 063 * <p> 064 * Assignment inside a condition is not a problem here, as the assignment is surrounded 065 * by an extra pair of parentheses. The comparison is {@code != null} and there is no chance that 066 * intention was to write {@code line == reader.readLine()}. 067 * </p> 068 * 069 * <p> 070 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 071 * </p> 072 * 073 * <p> 074 * Violation Message Keys: 075 * </p> 076 * <ul> 077 * <li> 078 * {@code assignment.inner.avoid} 079 * </li> 080 * </ul> 081 * 082 * @since 3.0 083 */ 084@StatelessCheck 085public class InnerAssignmentCheck 086 extends AbstractCheck { 087 088 /** 089 * A key is pointing to the warning message text in "messages.properties" 090 * file. 091 */ 092 public static final String MSG_KEY = "assignment.inner.avoid"; 093 094 /** 095 * Allowed AST types from an assignment AST node 096 * towards the root. 097 */ 098 private static final int[][] ALLOWED_ASSIGNMENT_CONTEXT = { 099 {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}