001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2014 Oliver Burn 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//////////////////////////////////////////////////////////////////////////////// 019package com.puppycrawl.tools.checkstyle.checks.coding; 020 021import com.puppycrawl.tools.checkstyle.api.Check; 022import com.puppycrawl.tools.checkstyle.api.DetailAST; 023import com.puppycrawl.tools.checkstyle.api.ScopeUtils; 024import com.puppycrawl.tools.checkstyle.api.TokenTypes; 025import com.puppycrawl.tools.checkstyle.checks.CheckUtils; 026import java.util.Arrays; 027 028/** 029 * <p> 030 * Checks for magic numbers. 031 * </p> 032 * <p> 033 * An example of how to configure the check to ignore 034 * numbers 0, 1, 1.5, 2: 035 * </p> 036 * <pre> 037 * <module name="MagicNumber"> 038 * <property name="ignoreNumbers" value="0, 1, 1.5, 2"/> 039 * <property name="ignoreHashCodeMethod" value="true"/> 040 * </module> 041 * </pre> 042 * @author Rick Giles 043 * @author Lars Kühne 044 * @author Daniel Solano Gómez 045 */ 046public class MagicNumberCheck extends Check 047{ 048 /** 049 * The token types that are allowed in the AST path from the 050 * number literal to the enclosing constant definition. 051 */ 052 private static final int[] ALLOWED_PATH_TOKENTYPES = { 053 TokenTypes.ASSIGN, 054 TokenTypes.ARRAY_INIT, 055 TokenTypes.EXPR, 056 TokenTypes.UNARY_PLUS, 057 TokenTypes.UNARY_MINUS, 058 TokenTypes.TYPECAST, 059 TokenTypes.ELIST, 060 TokenTypes.LITERAL_NEW, 061 TokenTypes.METHOD_CALL, 062 TokenTypes.STAR, 063 }; 064 065 static { 066 Arrays.sort(ALLOWED_PATH_TOKENTYPES); 067 } 068 069 /** the numbers to ignore in the check, sorted */ 070 private double[] ignoreNumbers = {-1, 0, 1, 2}; 071 /** Whether to ignore magic numbers in a hash code method. */ 072 private boolean ignoreHashCodeMethod; 073 /** Whether to ignore magic numbers in annotation. */ 074 private boolean ignoreAnnotation; 075 076 @Override 077 public int[] getDefaultTokens() 078 { 079 return new int[] { 080 TokenTypes.NUM_DOUBLE, 081 TokenTypes.NUM_FLOAT, 082 TokenTypes.NUM_INT, 083 TokenTypes.NUM_LONG, 084 }; 085 } 086 087 @Override 088 public void visitToken(DetailAST ast) 089 { 090 if (ignoreAnnotation && isInAnnotation(ast)) { 091 return; 092 } 093 094 if (inIgnoreList(ast) 095 || (ignoreHashCodeMethod && isInHashCodeMethod(ast))) 096 { 097 return; 098 } 099 100 final DetailAST constantDefAST = findContainingConstantDef(ast); 101 102 if (constantDefAST == null) { 103 reportMagicNumber(ast); 104 } 105 else { 106 DetailAST astNode = ast.getParent(); 107 while (astNode != constantDefAST) { 108 final int type = astNode.getType(); 109 if (Arrays.binarySearch(ALLOWED_PATH_TOKENTYPES, type) < 0) { 110 reportMagicNumber(ast); 111 break; 112 } 113 114 astNode = astNode.getParent(); 115 } 116 } 117 } 118 119 /** 120 * Finds the constant definition that contains aAST. 121 * @param ast the AST 122 * @return the constant def or null if ast is not 123 * contained in a constant definition 124 */ 125 private DetailAST findContainingConstantDef(DetailAST ast) 126 { 127 DetailAST varDefAST = ast; 128 while ((varDefAST != null) 129 && (varDefAST.getType() != TokenTypes.VARIABLE_DEF) 130 && (varDefAST.getType() != TokenTypes.ENUM_CONSTANT_DEF)) 131 { 132 varDefAST = varDefAST.getParent(); 133 } 134 135 // no containing variable definition? 136 if (varDefAST == null) { 137 return null; 138 } 139 140 // implicit constant? 141 if (ScopeUtils.inInterfaceOrAnnotationBlock(varDefAST) 142 || (varDefAST.getType() == TokenTypes.ENUM_CONSTANT_DEF)) 143 { 144 return varDefAST; 145 } 146 147 // explicit constant 148 final DetailAST modifiersAST = 149 varDefAST.findFirstToken(TokenTypes.MODIFIERS); 150 if (modifiersAST.branchContains(TokenTypes.FINAL)) { 151 return varDefAST; 152 } 153 154 return null; 155 } 156 157 /** 158 * Reports aAST as a magic number, includes unary operators as needed. 159 * @param ast the AST node that contains the number to report 160 */ 161 private void reportMagicNumber(DetailAST ast) 162 { 163 String text = ast.getText(); 164 final DetailAST parent = ast.getParent(); 165 DetailAST reportAST = ast; 166 if (parent.getType() == TokenTypes.UNARY_MINUS) { 167 reportAST = parent; 168 text = "-" + text; 169 } 170 else if (parent.getType() == TokenTypes.UNARY_PLUS) { 171 reportAST = parent; 172 text = "+" + text; 173 } 174 log(reportAST.getLineNo(), 175 reportAST.getColumnNo(), 176 "magic.number", 177 text); 178 } 179 180 /** 181 * Determines whether or not the given AST is in a valid hash code method. 182 * A valid hash code method is considered to be a method of the signature 183 * {@code public int hashCode()}. 184 * 185 * @param ast the AST from which to search for an enclosing hash code 186 * method definition 187 * 188 * @return {@code true} if {@code ast} is in the scope of a valid hash 189 * code method 190 */ 191 private boolean isInHashCodeMethod(DetailAST ast) 192 { 193 // if not in a code block, can't be in hashCode() 194 if (!ScopeUtils.inCodeBlock(ast)) { 195 return false; 196 } 197 198 // find the method definition AST 199 DetailAST methodDefAST = ast.getParent(); 200 while ((null != methodDefAST) 201 && (TokenTypes.METHOD_DEF != methodDefAST.getType())) 202 { 203 methodDefAST = methodDefAST.getParent(); 204 } 205 206 if (null == methodDefAST) { 207 return false; 208 } 209 210 // Check for 'hashCode' name. 211 final DetailAST identAST = 212 methodDefAST.findFirstToken(TokenTypes.IDENT); 213 if (!"hashCode".equals(identAST.getText())) { 214 return false; 215 } 216 217 // Check for no arguments. 218 final DetailAST paramAST = 219 methodDefAST.findFirstToken(TokenTypes.PARAMETERS); 220 if (0 != paramAST.getChildCount()) { 221 return false; 222 } 223 224 // we are in a 'public int hashCode()' method! The compiler will ensure 225 // the method returns an 'int' and is public. 226 return true; 227 } 228 229 /** 230 * Decides whether the number of an AST is in the ignore list of this 231 * check. 232 * @param ast the AST to check 233 * @return true if the number of ast is in the ignore list of this 234 * check. 235 */ 236 private boolean inIgnoreList(DetailAST ast) 237 { 238 double value = CheckUtils.parseDouble(ast.getText(), ast.getType()); 239 final DetailAST parent = ast.getParent(); 240 if (parent.getType() == TokenTypes.UNARY_MINUS) { 241 value = -1 * value; 242 } 243 return (Arrays.binarySearch(ignoreNumbers, value) >= 0); 244 } 245 246 /** 247 * Sets the numbers to ignore in the check. 248 * BeanUtils converts numeric token list to double array automatically. 249 * @param list list of numbers to ignore. 250 */ 251 public void setIgnoreNumbers(double[] list) 252 { 253 if ((list == null) || (list.length == 0)) { 254 ignoreNumbers = new double[0]; 255 } 256 else { 257 ignoreNumbers = new double[list.length]; 258 System.arraycopy(list, 0, ignoreNumbers, 0, list.length); 259 Arrays.sort(ignoreNumbers); 260 } 261 } 262 263 /** 264 * Set whether to ignore hashCode methods. 265 * @param ignoreHashCodeMethod decide whether to ignore 266 * hash code methods 267 */ 268 public void setIgnoreHashCodeMethod(boolean ignoreHashCodeMethod) 269 { 270 this.ignoreHashCodeMethod = ignoreHashCodeMethod; 271 } 272 273 /** 274 * Set whether to ignore Annotations. 275 * @param ignoreAnnotation decide whether to ignore annotations 276 */ 277 public void setIgnoreAnnotation(boolean ignoreAnnotation) 278 { 279 this.ignoreAnnotation = ignoreAnnotation; 280 } 281 282 /** 283 * Determines if the column displays a token type of annotation or 284 * annotation member 285 * 286 * @param ast the AST from which to search for annotations 287 * 288 * @return {@code true} if the token type for this node is a annotation 289 */ 290 private boolean isInAnnotation(DetailAST ast) 291 { 292 if ((null == ast.getParent()) 293 || (null == ast.getParent().getParent())) 294 { 295 return false; 296 } 297 298 return (TokenTypes.ANNOTATION == ast.getParent().getParent().getType()) 299 || (TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR 300 == ast.getParent().getParent().getType()); 301 } 302}