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 * &lt;module name="MagicNumber"&gt;
038 *    &lt;property name="ignoreNumbers" value="0, 1, 1.5, 2"/&gt;
039 *    &lt;property name="ignoreHashCodeMethod" value="true"/&gt;
040 * &lt;/module&gt;
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}