View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2018 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.coding;
21  
22  import java.util.Arrays;
23  
24  import com.puppycrawl.tools.checkstyle.StatelessCheck;
25  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
26  import com.puppycrawl.tools.checkstyle.api.DetailAST;
27  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
28  import com.puppycrawl.tools.checkstyle.utils.CheckUtils;
29  import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
30  import com.puppycrawl.tools.checkstyle.utils.ScopeUtils;
31  import com.puppycrawl.tools.checkstyle.utils.TokenUtils;
32  
33  /**
34   * <p>
35   * Checks that there are no <a href="https://en.wikipedia.org/wiki/Magic_number_%28programming%29">
36   * &quot;magic numbers&quot;</a> where a magic
37   * number is a numeric literal that is not defined as a constant.
38   * By default, -1, 0, 1, and 2 are not considered to be magic numbers.
39   * </p>
40   *
41   * <p>Constant definition is any variable/field that has 'final' modifier.
42   * It is fine to have one constant defining multiple numeric literals within one expression:
43   * <pre>
44   * {@code static final int SECONDS_PER_DAY = 24 * 60 * 60;
45   * static final double SPECIAL_RATIO = 4.0 / 3.0;
46   * static final double SPECIAL_SUM = 1 + Math.E;
47   * static final double SPECIAL_DIFFERENCE = 4 - Math.PI;
48   * static final Border STANDARD_BORDER = BorderFactory.createEmptyBorder(3, 3, 3, 3);
49   * static final Integer ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE = new Integer(42);}
50   * </pre>
51   *
52   * <p>Check have following options:
53   * ignoreHashCodeMethod - ignore magic numbers in hashCode methods;
54   * ignoreAnnotation - ignore magic numbers in annotation declarations;
55   * ignoreFieldDeclaration - ignore magic numbers in field declarations.
56   * <p>
57   * To configure the check with default configuration:
58   * </p>
59   * <pre>
60   * &lt;module name=&quot;MagicNumber&quot;/&gt;
61   * </pre>
62   * <p>
63   * results is following violations:
64   * </p>
65   * <pre>
66   * {@code
67   *   {@literal @}MyAnnotation(6) // violation
68   *   class MyClass {
69   *       private field = 7; // violation
70   *
71   *       void foo() {
72   *          int i = i + 1; // no violation
73   *          int j = j + 8; // violation
74   *       }
75   *   }
76   * }
77   * </pre>
78   * <p>
79   * To configure the check so that it checks floating-point numbers
80   * that are not 0, 0.5, or 1:
81   * </p>
82   * <pre>
83   *   &lt;module name=&quot;MagicNumber&quot;&gt;
84   *       &lt;property name=&quot;tokens&quot; value=&quot;NUM_DOUBLE, NUM_FLOAT&quot;/&gt;
85   *       &lt;property name=&quot;ignoreNumbers&quot; value=&quot;0, 0.5, 1&quot;/&gt;
86   *       &lt;property name=&quot;ignoreFieldDeclaration&quot; value=&quot;true&quot;/&gt;
87   *       &lt;property name=&quot;ignoreAnnotation&quot; value=&quot;true&quot;/&gt;
88   *   &lt;/module&gt;
89   * </pre>
90   * <p>
91   * results is following violations:
92   * </p>
93   * <pre>
94   * {@code
95   *   {@literal @}MyAnnotation(6) // no violation
96   *   class MyClass {
97   *       private field = 7; // no violation
98   *
99   *       void foo() {
100  *          int i = i + 1; // no violation
101  *          int j = j + (int)0.5; // no violation
102  *       }
103  *   }
104  * }
105  * </pre>
106  * <p>
107  * Config example of constantWaiverParentToken option:
108  * </p>
109  * <pre>
110  *   &lt;module name=&quot;MagicNumber&quot;&gt;
111  *       &lt;property name=&quot;constantWaiverParentToken&quot; value=&quot;ASSIGN,ARRAY_INIT,EXPR,
112  *       UNARY_PLUS, UNARY_MINUS, TYPECAST, ELIST, DIV, PLUS &quot;/&gt;
113  *   &lt;/module&gt;
114  * </pre>
115  * <p>
116  * result is following violation:
117  * </p>
118  * <pre>
119  * {@code
120  * class TestMethodCall {
121  *     public void method2() {
122  *         final TestMethodCall dummyObject = new TestMethodCall(62);    //violation
123  *         final int a = 3;        // ok as waiver is ASSIGN
124  *         final int [] b = {4, 5} // ok as waiver is ARRAY_INIT
125  *         final int c = -3;       // ok as waiver is UNARY_MINUS
126  *         final int d = +4;       // ok as waiver is UNARY_PLUS
127  *         final int e = method(1, 2) // ELIST is there but violation due to METHOD_CALL
128  *         final int x = 3 * 4;    // violation
129  *         final int y = 3 / 4;    // ok as waiver is DIV
130  *         final int z = 3 + 4;    // ok as waiver is PLUS
131  *         final int w = 3 - 4;    // violation
132  *         final int x = (int)(3.4);    //ok as waiver is TYPECAST
133  *     }
134  * }
135  * }
136  * </pre>
137  * @author Rick Giles
138  * @author Lars Kühne
139  * @author Daniel Solano Gómez
140  */
141 @StatelessCheck
142 public class MagicNumberCheck extends AbstractCheck {
143 
144     /**
145      * A key is pointing to the warning message text in "messages.properties"
146      * file.
147      */
148     public static final String MSG_KEY = "magic.number";
149 
150     /**
151      * The token types that are allowed in the AST path from the
152      * number literal to the enclosing constant definition.
153      */
154     private int[] constantWaiverParentToken = {
155         TokenTypes.ASSIGN,
156         TokenTypes.ARRAY_INIT,
157         TokenTypes.EXPR,
158         TokenTypes.UNARY_PLUS,
159         TokenTypes.UNARY_MINUS,
160         TokenTypes.TYPECAST,
161         TokenTypes.ELIST,
162         TokenTypes.LITERAL_NEW,
163         TokenTypes.METHOD_CALL,
164         TokenTypes.STAR,
165         TokenTypes.DIV,
166         TokenTypes.PLUS,
167         TokenTypes.MINUS,
168     };
169 
170     /** The numbers to ignore in the check, sorted. */
171     private double[] ignoreNumbers = {-1, 0, 1, 2};
172 
173     /** Whether to ignore magic numbers in a hash code method. */
174     private boolean ignoreHashCodeMethod;
175 
176     /** Whether to ignore magic numbers in annotation. */
177     private boolean ignoreAnnotation;
178 
179     /** Whether to ignore magic numbers in field declaration. */
180     private boolean ignoreFieldDeclaration;
181 
182     /**
183      * Constructor for MagicNumber Check.
184      * Sort the allowedTokensBetweenMagicNumberAndConstDef array for binary search.
185      */
186     public MagicNumberCheck() {
187         Arrays.sort(constantWaiverParentToken);
188     }
189 
190     @Override
191     public int[] getDefaultTokens() {
192         return getAcceptableTokens();
193     }
194 
195     @Override
196     public int[] getAcceptableTokens() {
197         return new int[] {
198             TokenTypes.NUM_DOUBLE,
199             TokenTypes.NUM_FLOAT,
200             TokenTypes.NUM_INT,
201             TokenTypes.NUM_LONG,
202         };
203     }
204 
205     @Override
206     public int[] getRequiredTokens() {
207         return CommonUtils.EMPTY_INT_ARRAY;
208     }
209 
210     @Override
211     public void visitToken(DetailAST ast) {
212         if ((!ignoreAnnotation || !isChildOf(ast, TokenTypes.ANNOTATION))
213                 && !isInIgnoreList(ast)
214                 && (!ignoreHashCodeMethod || !isInHashCodeMethod(ast))) {
215             final DetailAST constantDefAST = findContainingConstantDef(ast);
216 
217             if (constantDefAST == null) {
218                 if (!ignoreFieldDeclaration || !isFieldDeclaration(ast)) {
219                     reportMagicNumber(ast);
220                 }
221             }
222             else {
223                 final boolean found = isMagicNumberExists(ast, constantDefAST);
224                 if (found) {
225                     reportMagicNumber(ast);
226                 }
227             }
228         }
229     }
230 
231     /**
232      * Is magic number some where at ast tree.
233      * @param ast ast token
234      * @param constantDefAST constant ast
235      * @return true if magic number is present
236      */
237     private boolean isMagicNumberExists(DetailAST ast, DetailAST constantDefAST) {
238         boolean found = false;
239         DetailAST astNode = ast.getParent();
240         while (astNode != constantDefAST) {
241             final int type = astNode.getType();
242             if (Arrays.binarySearch(constantWaiverParentToken, type) < 0) {
243                 found = true;
244                 break;
245             }
246             astNode = astNode.getParent();
247         }
248         return found;
249     }
250 
251     /**
252      * Finds the constant definition that contains aAST.
253      * @param ast the AST
254      * @return the constant def or null if ast is not contained in a constant definition.
255      */
256     private static DetailAST findContainingConstantDef(DetailAST ast) {
257         DetailAST varDefAST = ast;
258         while (varDefAST != null
259                 && varDefAST.getType() != TokenTypes.VARIABLE_DEF
260                 && varDefAST.getType() != TokenTypes.ENUM_CONSTANT_DEF) {
261             varDefAST = varDefAST.getParent();
262         }
263         DetailAST constantDef = null;
264 
265         // no containing variable definition?
266         if (varDefAST != null) {
267             // implicit constant?
268             if (ScopeUtils.isInInterfaceOrAnnotationBlock(varDefAST)
269                     || varDefAST.getType() == TokenTypes.ENUM_CONSTANT_DEF) {
270                 constantDef = varDefAST;
271             }
272             else {
273                 // explicit constant
274                 final DetailAST modifiersAST = varDefAST.findFirstToken(TokenTypes.MODIFIERS);
275 
276                 if (modifiersAST.findFirstToken(TokenTypes.FINAL) != null) {
277                     constantDef = varDefAST;
278                 }
279             }
280         }
281         return constantDef;
282     }
283 
284     /**
285      * Reports aAST as a magic number, includes unary operators as needed.
286      * @param ast the AST node that contains the number to report
287      */
288     private void reportMagicNumber(DetailAST ast) {
289         String text = ast.getText();
290         final DetailAST parent = ast.getParent();
291         DetailAST reportAST = ast;
292         if (parent.getType() == TokenTypes.UNARY_MINUS) {
293             reportAST = parent;
294             text = "-" + text;
295         }
296         else if (parent.getType() == TokenTypes.UNARY_PLUS) {
297             reportAST = parent;
298             text = "+" + text;
299         }
300         log(reportAST.getLineNo(),
301                 reportAST.getColumnNo(),
302                 MSG_KEY,
303                 text);
304     }
305 
306     /**
307      * Determines whether or not the given AST is in a valid hash code method.
308      * A valid hash code method is considered to be a method of the signature
309      * {@code public int hashCode()}.
310      *
311      * @param ast the AST from which to search for an enclosing hash code
312      *     method definition
313      *
314      * @return {@code true} if {@code ast} is in the scope of a valid hash code method.
315      */
316     private static boolean isInHashCodeMethod(DetailAST ast) {
317         boolean inHashCodeMethod = false;
318 
319         // if not in a code block, can't be in hashCode()
320         if (ScopeUtils.isInCodeBlock(ast)) {
321             // find the method definition AST
322             DetailAST methodDefAST = ast.getParent();
323             while (methodDefAST != null
324                     && methodDefAST.getType() != TokenTypes.METHOD_DEF) {
325                 methodDefAST = methodDefAST.getParent();
326             }
327 
328             if (methodDefAST != null) {
329                 // Check for 'hashCode' name.
330                 final DetailAST identAST = methodDefAST.findFirstToken(TokenTypes.IDENT);
331 
332                 if ("hashCode".equals(identAST.getText())) {
333                     // Check for no arguments.
334                     final DetailAST paramAST = methodDefAST.findFirstToken(TokenTypes.PARAMETERS);
335                     // we are in a 'public int hashCode()' method! The compiler will ensure
336                     // the method returns an 'int' and is public.
337                     inHashCodeMethod = paramAST.getChildCount() == 0;
338                 }
339             }
340         }
341         return inHashCodeMethod;
342     }
343 
344     /**
345      * Decides whether the number of an AST is in the ignore list of this
346      * check.
347      * @param ast the AST to check
348      * @return true if the number of ast is in the ignore list of this check.
349      */
350     private boolean isInIgnoreList(DetailAST ast) {
351         double value = CheckUtils.parseDouble(ast.getText(), ast.getType());
352         final DetailAST parent = ast.getParent();
353         if (parent.getType() == TokenTypes.UNARY_MINUS) {
354             value = -1 * value;
355         }
356         return Arrays.binarySearch(ignoreNumbers, value) >= 0;
357     }
358 
359     /**
360      * Determines whether or not the given AST is field declaration.
361      *
362      * @param ast AST from which to search for an enclosing field declaration
363      *
364      * @return {@code true} if {@code ast} is in the scope of field declaration
365      */
366     private static boolean isFieldDeclaration(DetailAST ast) {
367         DetailAST varDefAST = ast;
368         while (varDefAST != null
369                 && varDefAST.getType() != TokenTypes.VARIABLE_DEF) {
370             varDefAST = varDefAST.getParent();
371         }
372 
373         // contains variable declaration
374         // and it is directly inside class declaration
375         return varDefAST != null
376                 && varDefAST.getParent().getParent().getType() == TokenTypes.CLASS_DEF;
377     }
378 
379     /**
380      * Sets the tokens which are allowed between Magic Number and defined Object.
381      * @param tokens The string representation of the tokens interested in
382      */
383     public void setConstantWaiverParentToken(String... tokens) {
384         constantWaiverParentToken = new int[tokens.length];
385         for (int i = 0; i < tokens.length; i++) {
386             constantWaiverParentToken[i] = TokenUtils.getTokenId(tokens[i]);
387         }
388         Arrays.sort(constantWaiverParentToken);
389     }
390 
391     /**
392      * Sets the numbers to ignore in the check.
393      * BeanUtils converts numeric token list to double array automatically.
394      * @param list list of numbers to ignore.
395      */
396     public void setIgnoreNumbers(double... list) {
397         if (list.length == 0) {
398             ignoreNumbers = CommonUtils.EMPTY_DOUBLE_ARRAY;
399         }
400         else {
401             ignoreNumbers = new double[list.length];
402             System.arraycopy(list, 0, ignoreNumbers, 0, list.length);
403             Arrays.sort(ignoreNumbers);
404         }
405     }
406 
407     /**
408      * Set whether to ignore hashCode methods.
409      * @param ignoreHashCodeMethod decide whether to ignore
410      *     hash code methods
411      */
412     public void setIgnoreHashCodeMethod(boolean ignoreHashCodeMethod) {
413         this.ignoreHashCodeMethod = ignoreHashCodeMethod;
414     }
415 
416     /**
417      * Set whether to ignore Annotations.
418      * @param ignoreAnnotation decide whether to ignore annotations
419      */
420     public void setIgnoreAnnotation(boolean ignoreAnnotation) {
421         this.ignoreAnnotation = ignoreAnnotation;
422     }
423 
424     /**
425      * Set whether to ignore magic numbers in field declaration.
426      * @param ignoreFieldDeclaration decide whether to ignore magic numbers
427      *     in field declaration
428      */
429     public void setIgnoreFieldDeclaration(boolean ignoreFieldDeclaration) {
430         this.ignoreFieldDeclaration = ignoreFieldDeclaration;
431     }
432 
433     /**
434      * Determines if the given AST node has a parent node with given token type code.
435      *
436      * @param ast the AST from which to search for annotations
437      * @param type the type code of parent token
438      *
439      * @return {@code true} if the AST node has a parent with given token type.
440      */
441     private static boolean isChildOf(DetailAST ast, int type) {
442         boolean result = false;
443         DetailAST node = ast;
444         do {
445             if (node.getType() == type) {
446                 result = true;
447                 break;
448             }
449             node = node.getParent();
450         } while (node != null);
451 
452         return result;
453     }
454 
455 }