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