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 * <p> 033 * Checks for assignments in subexpressions, such as in 034 * {@code String s = Integer.toString(i = 2);}. 035 * </p> 036 * <p> 037 * Rationale: Except for the loop idioms, 038 * all assignments should occur in their own top-level statement to increase readability. 039 * With inner assignments like the one given above, it is difficult to see all places 040 * where a variable is set. 041 * </p> 042 * <p> 043 * Note: Check allows usage of the popular assignments in loops: 044 * </p> 045 * <pre> 046 * String line; 047 * while ((line = bufferedReader.readLine()) != null) { // OK 048 * // process the line 049 * } 050 * 051 * for (;(line = bufferedReader.readLine()) != null;) { // OK 052 * // process the line 053 * } 054 * 055 * do { 056 * // process the line 057 * } 058 * while ((line = bufferedReader.readLine()) != null); // OK 059 * </pre> 060 * <p> 061 * Assignment inside a condition is not a problem here, as the assignment is surrounded 062 * by an extra pair of parentheses. The comparison is {@code != null} and there is no chance that 063 * intention was to write {@code line == reader.readLine()}. 064 * </p> 065 * <p> 066 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 067 * </p> 068 * <p> 069 * Violation Message Keys: 070 * </p> 071 * <ul> 072 * <li> 073 * {@code assignment.inner.avoid} 074 * </li> 075 * </ul> 076 * 077 * @since 3.0 078 */ 079@StatelessCheck 080public class InnerAssignmentCheck 081 extends AbstractCheck { 082 083 /** 084 * A key is pointing to the warning message text in "messages.properties" 085 * file. 086 */ 087 public static final String MSG_KEY = "assignment.inner.avoid"; 088 089 /** 090 * Allowed AST types from an assignment AST node 091 * towards the root. 092 */ 093 private static final int[][] ALLOWED_ASSIGNMENT_CONTEXT = { 094 {TokenTypes.EXPR, TokenTypes.SLIST}, 095 {TokenTypes.VARIABLE_DEF}, 096 {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_INIT}, 097 {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_ITERATOR}, 098 {TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR}, { 099 TokenTypes.RESOURCE, 100 TokenTypes.RESOURCES, 101 TokenTypes.RESOURCE_SPECIFICATION, 102 }, 103 {TokenTypes.EXPR, TokenTypes.LAMBDA}, 104 }; 105 106 /** 107 * Allowed AST types from an assignment AST node 108 * towards the root. 109 */ 110 private static final int[][] CONTROL_CONTEXT = { 111 {TokenTypes.EXPR, TokenTypes.LITERAL_DO}, 112 {TokenTypes.EXPR, TokenTypes.LITERAL_FOR}, 113 {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE}, 114 {TokenTypes.EXPR, TokenTypes.LITERAL_IF}, 115 {TokenTypes.EXPR, TokenTypes.LITERAL_ELSE}, 116 }; 117 118 /** 119 * Allowed AST types from a comparison node (above an assignment) 120 * towards the root. 121 */ 122 private static final int[][] ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT = { 123 {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE}, 124 {TokenTypes.EXPR, TokenTypes.FOR_CONDITION}, 125 {TokenTypes.EXPR, TokenTypes.LITERAL_DO}, 126 }; 127 128 /** 129 * The token types that identify comparison operators. 130 */ 131 private static final BitSet COMPARISON_TYPES = TokenUtil.asBitSet( 132 TokenTypes.EQUAL, 133 TokenTypes.GE, 134 TokenTypes.GT, 135 TokenTypes.LE, 136 TokenTypes.LT, 137 TokenTypes.NOT_EQUAL 138 ); 139 140 /** 141 * The token types that are ignored while checking "loop-idiom". 142 */ 143 private static final BitSet LOOP_IDIOM_IGNORED_PARENTS = TokenUtil.asBitSet( 144 TokenTypes.LAND, 145 TokenTypes.LOR, 146 TokenTypes.LNOT, 147 TokenTypes.BOR, 148 TokenTypes.BAND 149 ); 150 151 @Override 152 public int[] getDefaultTokens() { 153 return getRequiredTokens(); 154 } 155 156 @Override 157 public int[] getAcceptableTokens() { 158 return getRequiredTokens(); 159 } 160 161 @Override 162 public int[] getRequiredTokens() { 163 return new int[] { 164 TokenTypes.ASSIGN, // '=' 165 TokenTypes.DIV_ASSIGN, // "/=" 166 TokenTypes.PLUS_ASSIGN, // "+=" 167 TokenTypes.MINUS_ASSIGN, // "-=" 168 TokenTypes.STAR_ASSIGN, // "*=" 169 TokenTypes.MOD_ASSIGN, // "%=" 170 TokenTypes.SR_ASSIGN, // ">>=" 171 TokenTypes.BSR_ASSIGN, // ">>>=" 172 TokenTypes.SL_ASSIGN, // "<<=" 173 TokenTypes.BXOR_ASSIGN, // "^=" 174 TokenTypes.BOR_ASSIGN, // "|=" 175 TokenTypes.BAND_ASSIGN, // "&=" 176 }; 177 } 178 179 @Override 180 public void visitToken(DetailAST ast) { 181 if (!isInContext(ast, ALLOWED_ASSIGNMENT_CONTEXT, CommonUtil.EMPTY_BIT_SET) 182 && !isInNoBraceControlStatement(ast) 183 && !isInLoopIdiom(ast)) { 184 log(ast, MSG_KEY); 185 } 186 } 187 188 /** 189 * Determines if ast is in the body of a flow control statement without 190 * braces. An example of such a statement would be 191 * <pre> 192 * if (y < 0) 193 * x = y; 194 * </pre> 195 * <p> 196 * This leads to the following AST structure: 197 * </p> 198 * <pre> 199 * LITERAL_IF 200 * LPAREN 201 * EXPR // test 202 * RPAREN 203 * EXPR // body 204 * SEMI 205 * </pre> 206 * <p> 207 * We need to ensure that ast is in the body and not in the test. 208 * </p> 209 * 210 * @param ast an assignment operator AST 211 * @return whether ast is in the body of a flow control statement 212 */ 213 private static boolean isInNoBraceControlStatement(DetailAST ast) { 214 boolean result = false; 215 if (isInContext(ast, CONTROL_CONTEXT, CommonUtil.EMPTY_BIT_SET)) { 216 final DetailAST expr = ast.getParent(); 217 final DetailAST exprNext = expr.getNextSibling(); 218 result = exprNext.getType() == TokenTypes.SEMI; 219 } 220 return result; 221 } 222 223 /** 224 * Tests whether the given AST is used in the "assignment in loop" idiom. 225 * <pre> 226 * String line; 227 * while ((line = bufferedReader.readLine()) != null) { 228 * // process the line 229 * } 230 * for (;(line = bufferedReader.readLine()) != null;) { 231 * // process the line 232 * } 233 * do { 234 * // process the line 235 * } 236 * while ((line = bufferedReader.readLine()) != null); 237 * </pre> 238 * Assignment inside a condition is not a problem here, as the assignment is surrounded by an 239 * extra pair of parentheses. The comparison is {@code != null} and there is no chance that 240 * intention was to write {@code line == reader.readLine()}. 241 * 242 * @param ast assignment AST 243 * @return whether the context of the assignment AST indicates the idiom 244 */ 245 private static boolean isInLoopIdiom(DetailAST ast) { 246 return isComparison(ast.getParent()) 247 && isInContext(ast.getParent(), 248 ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT, 249 LOOP_IDIOM_IGNORED_PARENTS); 250 } 251 252 /** 253 * Checks if an AST is a comparison operator. 254 * 255 * @param ast the AST to check 256 * @return true iff ast is a comparison operator. 257 */ 258 private static boolean isComparison(DetailAST ast) { 259 final int astType = ast.getType(); 260 return COMPARISON_TYPES.get(astType); 261 } 262 263 /** 264 * Tests whether the provided AST is in 265 * one of the given contexts. 266 * 267 * @param ast the AST from which to start walking towards root 268 * @param contextSet the contexts to test against. 269 * @param skipTokens parent token types to ignore 270 * 271 * @return whether the parents nodes of ast match one of the allowed type paths. 272 */ 273 private static boolean isInContext(DetailAST ast, int[][] contextSet, BitSet skipTokens) { 274 boolean found = false; 275 for (int[] element : contextSet) { 276 DetailAST current = ast; 277 for (int anElement : element) { 278 current = getParent(current, skipTokens); 279 if (current.getType() == anElement) { 280 found = true; 281 } 282 else { 283 found = false; 284 break; 285 } 286 } 287 288 if (found) { 289 break; 290 } 291 } 292 return found; 293 } 294 295 /** 296 * Get ast parent, ignoring token types from {@code skipTokens}. 297 * 298 * @param ast token to get parent 299 * @param skipTokens token types to skip 300 * @return first not ignored parent of ast 301 */ 302 private static DetailAST getParent(DetailAST ast, BitSet skipTokens) { 303 DetailAST result = ast.getParent(); 304 while (skipTokens.get(result.getType())) { 305 result = result.getParent(); 306 } 307 return result; 308 } 309 310}