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