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