001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2018 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 antlr.collections.AST; 025import com.puppycrawl.tools.checkstyle.StatelessCheck; 026import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.TokenTypes; 029 030/** 031 * <p> 032 * Checks for assignments in subexpressions, such as in 033 * {@code String s = Integer.toString(i = 2);}. 034 * </p> 035 * <p> 036 * Rationale: With the exception of {@code for} iterators, all assignments 037 * should occur in their own top-level statement to increase readability. 038 * With inner assignments like the above it is difficult to see all places 039 * where a variable is set. 040 * </p> 041 * 042 * @author lkuehne 043 */ 044@StatelessCheck 045public class InnerAssignmentCheck 046 extends AbstractCheck { 047 048 /** 049 * A key is pointing to the warning message text in "messages.properties" 050 * file. 051 */ 052 public static final String MSG_KEY = "assignment.inner.avoid"; 053 054 /** 055 * List of allowed AST types from an assignment AST node 056 * towards the root. 057 */ 058 private static final int[][] ALLOWED_ASSIGNMENT_CONTEXT = { 059 {TokenTypes.EXPR, TokenTypes.SLIST}, 060 {TokenTypes.VARIABLE_DEF}, 061 {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_INIT}, 062 {TokenTypes.EXPR, TokenTypes.ELIST, TokenTypes.FOR_ITERATOR}, 063 {TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR}, { 064 TokenTypes.RESOURCE, 065 TokenTypes.RESOURCES, 066 TokenTypes.RESOURCE_SPECIFICATION, 067 }, 068 {TokenTypes.EXPR, TokenTypes.LAMBDA}, 069 }; 070 071 /** 072 * List of allowed AST types from an assignment AST node 073 * towards the root. 074 */ 075 private static final int[][] CONTROL_CONTEXT = { 076 {TokenTypes.EXPR, TokenTypes.LITERAL_DO}, 077 {TokenTypes.EXPR, TokenTypes.LITERAL_FOR}, 078 {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE}, 079 {TokenTypes.EXPR, TokenTypes.LITERAL_IF}, 080 {TokenTypes.EXPR, TokenTypes.LITERAL_ELSE}, 081 }; 082 083 /** 084 * List of allowed AST types from a comparison node (above an assignment) 085 * towards the root. 086 */ 087 private static final int[][] ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT = { 088 {TokenTypes.EXPR, TokenTypes.LITERAL_WHILE, }, 089 }; 090 091 /** 092 * The token types that identify comparison operators. 093 */ 094 private static final int[] COMPARISON_TYPES = { 095 TokenTypes.EQUAL, 096 TokenTypes.GE, 097 TokenTypes.GT, 098 TokenTypes.LE, 099 TokenTypes.LT, 100 TokenTypes.NOT_EQUAL, 101 }; 102 103 static { 104 Arrays.sort(COMPARISON_TYPES); 105 } 106 107 @Override 108 public int[] getDefaultTokens() { 109 return getRequiredTokens(); 110 } 111 112 @Override 113 public int[] getAcceptableTokens() { 114 return getRequiredTokens(); 115 } 116 117 @Override 118 public int[] getRequiredTokens() { 119 return new int[] { 120 TokenTypes.ASSIGN, // '=' 121 TokenTypes.DIV_ASSIGN, // "/=" 122 TokenTypes.PLUS_ASSIGN, // "+=" 123 TokenTypes.MINUS_ASSIGN, //"-=" 124 TokenTypes.STAR_ASSIGN, // "*=" 125 TokenTypes.MOD_ASSIGN, // "%=" 126 TokenTypes.SR_ASSIGN, // ">>=" 127 TokenTypes.BSR_ASSIGN, // ">>>=" 128 TokenTypes.SL_ASSIGN, // "<<=" 129 TokenTypes.BXOR_ASSIGN, // "^=" 130 TokenTypes.BOR_ASSIGN, // "|=" 131 TokenTypes.BAND_ASSIGN, // "&=" 132 }; 133 } 134 135 @Override 136 public void visitToken(DetailAST ast) { 137 if (!isInContext(ast, ALLOWED_ASSIGNMENT_CONTEXT) 138 && !isInNoBraceControlStatement(ast) 139 && !isInWhileIdiom(ast)) { 140 log(ast.getLineNo(), ast.getColumnNo(), MSG_KEY); 141 } 142 } 143 144 /** 145 * Determines if ast is in the body of a flow control statement without 146 * braces. An example of such a statement would be 147 * <p> 148 * <pre> 149 * if (y < 0) 150 * x = y; 151 * </pre> 152 * </p> 153 * <p> 154 * This leads to the following AST structure: 155 * </p> 156 * <p> 157 * <pre> 158 * LITERAL_IF 159 * LPAREN 160 * EXPR // test 161 * RPAREN 162 * EXPR // body 163 * SEMI 164 * </pre> 165 * </p> 166 * <p> 167 * We need to ensure that ast is in the body and not in the test. 168 * </p> 169 * 170 * @param ast an assignment operator AST 171 * @return whether ast is in the body of a flow control statement 172 */ 173 private static boolean isInNoBraceControlStatement(DetailAST ast) { 174 boolean result = false; 175 if (isInContext(ast, CONTROL_CONTEXT)) { 176 final DetailAST expr = ast.getParent(); 177 final AST exprNext = expr.getNextSibling(); 178 result = exprNext.getType() == TokenTypes.SEMI; 179 } 180 return result; 181 } 182 183 /** 184 * Tests whether the given AST is used in the "assignment in while" idiom. 185 * <pre> 186 * String line; 187 * while ((line = bufferedReader.readLine()) != null) { 188 * // process the line 189 * } 190 * </pre> 191 * Assignment inside a condition is not a problem here, as the assignment is surrounded by an 192 * extra pair of parentheses. The comparison is {@code != null} and there is no chance that 193 * intention was to write {@code line == reader.readLine()}. 194 * 195 * @param ast assignment AST 196 * @return whether the context of the assignment AST indicates the idiom 197 */ 198 private static boolean isInWhileIdiom(DetailAST ast) { 199 boolean result = false; 200 if (isComparison(ast.getParent())) { 201 result = isInContext( 202 ast.getParent(), ALLOWED_ASSIGNMENT_IN_COMPARISON_CONTEXT); 203 } 204 return result; 205 } 206 207 /** 208 * Checks if an AST is a comparison operator. 209 * @param ast the AST to check 210 * @return true iff ast is a comparison operator. 211 */ 212 private static boolean isComparison(DetailAST ast) { 213 final int astType = ast.getType(); 214 return Arrays.binarySearch(COMPARISON_TYPES, astType) >= 0; 215 } 216 217 /** 218 * Tests whether the provided AST is in 219 * one of the given contexts. 220 * 221 * @param ast the AST from which to start walking towards root 222 * @param contextSet the contexts to test against. 223 * 224 * @return whether the parents nodes of ast match one of the allowed type paths. 225 */ 226 private static boolean isInContext(DetailAST ast, int[]... contextSet) { 227 boolean found = false; 228 for (int[] element : contextSet) { 229 DetailAST current = ast; 230 for (int anElement : element) { 231 current = current.getParent(); 232 if (current.getType() == anElement) { 233 found = true; 234 } 235 else { 236 found = false; 237 break; 238 } 239 } 240 241 if (found) { 242 break; 243 } 244 } 245 return found; 246 } 247 248}