View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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  import java.util.BitSet;
24  
25  import com.puppycrawl.tools.checkstyle.PropertyType;
26  import com.puppycrawl.tools.checkstyle.StatelessCheck;
27  import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
28  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
29  import com.puppycrawl.tools.checkstyle.api.DetailAST;
30  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
31  import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
32  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
33  import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
34  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
35  
36  /**
37   * <p>
38   * Checks that there are no
39   * <a href="https://en.wikipedia.org/wiki/Magic_number_%28programming%29">
40   * &quot;magic numbers&quot;</a> where a magic
41   * number is a numeric literal that is not defined as a constant.
42   * By default, -1, 0, 1, and 2 are not considered to be magic numbers.
43   * </p>
44   *
45   * <p>Constant definition is any variable/field that has 'final' modifier.
46   * It is fine to have one constant defining multiple numeric literals within one expression:
47   * </p>
48   * <pre>
49   * static final int SECONDS_PER_DAY = 24 * 60 * 60;
50   * static final double SPECIAL_RATIO = 4.0 / 3.0;
51   * static final double SPECIAL_SUM = 1 + Math.E;
52   * static final double SPECIAL_DIFFERENCE = 4 - Math.PI;
53   * static final Border STANDARD_BORDER = BorderFactory.createEmptyBorder(3, 3, 3, 3);
54   * static final Integer ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE = new Integer(42);
55   * </pre>
56   * <ul>
57   * <li>
58   * Property {@code constantWaiverParentToken} - Specify tokens that are allowed in the AST path
59   * from the number literal to the enclosing constant definition.
60   * Type is {@code java.lang.String[]}.
61   * Validation type is {@code tokenTypesSet}.
62   * Default value is
63   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ARRAY_INIT">
64   * ARRAY_INIT</a>,
65   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ASSIGN">
66   * ASSIGN</a>,
67   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#DIV">
68   * DIV</a>,
69   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ELIST">
70   * ELIST</a>,
71   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#EXPR">
72   * EXPR</a>,
73   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_NEW">
74   * LITERAL_NEW</a>,
75   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_CALL">
76   * METHOD_CALL</a>,
77   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#MINUS">
78   * MINUS</a>,
79   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PLUS">
80   * PLUS</a>,
81   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#STAR">
82   * STAR</a>,
83   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#TYPECAST">
84   * TYPECAST</a>,
85   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#UNARY_MINUS">
86   * UNARY_MINUS</a>,
87   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#UNARY_PLUS">
88   * UNARY_PLUS</a>.
89   * </li>
90   * <li>
91   * Property {@code ignoreAnnotation} - Ignore magic numbers in annotation declarations.
92   * Type is {@code boolean}.
93   * Default value is {@code false}.
94   * </li>
95   * <li>
96   * Property {@code ignoreAnnotationElementDefaults} -
97   * Ignore magic numbers in annotation elements defaults.
98   * Type is {@code boolean}.
99   * Default value is {@code true}.
100  * </li>
101  * <li>
102  * Property {@code ignoreFieldDeclaration} - Ignore magic numbers in field declarations.
103  * Type is {@code boolean}.
104  * Default value is {@code false}.
105  * </li>
106  * <li>
107  * Property {@code ignoreHashCodeMethod} - Ignore magic numbers in hashCode methods.
108  * Type is {@code boolean}.
109  * Default value is {@code false}.
110  * </li>
111  * <li>
112  * Property {@code ignoreNumbers} - Specify non-magic numbers.
113  * Type is {@code double[]}.
114  * Default value is {@code -1, 0, 1, 2}.
115  * </li>
116  * <li>
117  * Property {@code tokens} - tokens to check
118  * Type is {@code java.lang.String[]}.
119  * Validation type is {@code tokenSet}.
120  * Default value is:
121  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#NUM_DOUBLE">
122  * NUM_DOUBLE</a>,
123  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#NUM_FLOAT">
124  * NUM_FLOAT</a>,
125  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#NUM_INT">
126  * NUM_INT</a>,
127  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#NUM_LONG">
128  * NUM_LONG</a>.
129  * </li>
130  * </ul>
131  * <p>
132  * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
133  * </p>
134  * <p>
135  * Violation Message Keys:
136  * </p>
137  * <ul>
138  * <li>
139  * {@code magic.number}
140  * </li>
141  * </ul>
142  *
143  * @since 3.1
144  */
145 @StatelessCheck
146 public class MagicNumberCheck extends AbstractCheck {
147 
148     /**
149      * A key is pointing to the warning message text in "messages.properties"
150      * file.
151      */
152     public static final String MSG_KEY = "magic.number";
153 
154     /**
155      * Specify tokens that are allowed in the AST path from the
156      * number literal to the enclosing constant definition.
157      */
158     @XdocsPropertyType(PropertyType.TOKEN_ARRAY)
159     private BitSet constantWaiverParentToken = TokenUtil.asBitSet(
160         TokenTypes.ASSIGN,
161         TokenTypes.ARRAY_INIT,
162         TokenTypes.EXPR,
163         TokenTypes.UNARY_PLUS,
164         TokenTypes.UNARY_MINUS,
165         TokenTypes.TYPECAST,
166         TokenTypes.ELIST,
167         TokenTypes.LITERAL_NEW,
168         TokenTypes.METHOD_CALL,
169         TokenTypes.STAR,
170         TokenTypes.DIV,
171         TokenTypes.PLUS,
172         TokenTypes.MINUS
173     );
174 
175     /** Specify non-magic numbers. */
176     private double[] ignoreNumbers = {-1, 0, 1, 2};
177 
178     /** Ignore magic numbers in hashCode methods. */
179     private boolean ignoreHashCodeMethod;
180 
181     /** Ignore magic numbers in annotation declarations. */
182     private boolean ignoreAnnotation;
183 
184     /** Ignore magic numbers in field declarations. */
185     private boolean ignoreFieldDeclaration;
186 
187     /** Ignore magic numbers in annotation elements defaults. */
188     private boolean ignoreAnnotationElementDefaults = true;
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 CommonUtil.EMPTY_INT_ARRAY;
208     }
209 
210     @Override
211     public void visitToken(DetailAST ast) {
212         if (shouldTestAnnotationArgs(ast)
213                 && shouldTestAnnotationDefaults(ast)
214                 && !isInIgnoreList(ast)
215                 && (!ignoreHashCodeMethod || !isInHashCodeMethod(ast))) {
216             final DetailAST constantDefAST = findContainingConstantDef(ast);
217 
218             if (constantDefAST == null) {
219                 if (!ignoreFieldDeclaration || !isFieldDeclaration(ast)) {
220                     reportMagicNumber(ast);
221                 }
222             }
223             else {
224                 final boolean found = isMagicNumberExists(ast, constantDefAST);
225                 if (found) {
226                     reportMagicNumber(ast);
227                 }
228             }
229         }
230     }
231 
232     /**
233      * Checks if ast is annotation argument and should be checked.
234      *
235      * @param ast token to check
236      * @return true if element is skipped, false otherwise
237      */
238     private boolean shouldTestAnnotationArgs(DetailAST ast) {
239         return !ignoreAnnotation || !isChildOf(ast, TokenTypes.ANNOTATION);
240     }
241 
242     /**
243      * Checks if ast is annotation element default value and should be checked.
244      *
245      * @param ast token to check
246      * @return true if element is skipped, false otherwise
247      */
248     private boolean shouldTestAnnotationDefaults(DetailAST ast) {
249         return !ignoreAnnotationElementDefaults || !isChildOf(ast, TokenTypes.LITERAL_DEFAULT);
250     }
251 
252     /**
253      * Is magic number somewhere at ast tree.
254      *
255      * @param ast ast token
256      * @param constantDefAST constant ast
257      * @return true if magic number is present
258      */
259     private boolean isMagicNumberExists(DetailAST ast, DetailAST constantDefAST) {
260         boolean found = false;
261         DetailAST astNode = ast.getParent();
262         while (astNode != constantDefAST) {
263             final int type = astNode.getType();
264             if (!constantWaiverParentToken.get(type)) {
265                 found = true;
266                 break;
267             }
268             astNode = astNode.getParent();
269         }
270         return found;
271     }
272 
273     /**
274      * Finds the constant definition that contains aAST.
275      *
276      * @param ast the AST
277      * @return the constant def or null if ast is not contained in a constant definition.
278      */
279     private static DetailAST findContainingConstantDef(DetailAST ast) {
280         DetailAST varDefAST = ast;
281         while (varDefAST != null
282                 && varDefAST.getType() != TokenTypes.VARIABLE_DEF
283                 && varDefAST.getType() != TokenTypes.ENUM_CONSTANT_DEF) {
284             varDefAST = varDefAST.getParent();
285         }
286         DetailAST constantDef = null;
287 
288         // no containing variable definition?
289         if (varDefAST != null) {
290             // implicit constant?
291             if (ScopeUtil.isInInterfaceOrAnnotationBlock(varDefAST)
292                     || varDefAST.getType() == TokenTypes.ENUM_CONSTANT_DEF) {
293                 constantDef = varDefAST;
294             }
295             else {
296                 // explicit constant
297                 final DetailAST modifiersAST = varDefAST.findFirstToken(TokenTypes.MODIFIERS);
298 
299                 if (modifiersAST.findFirstToken(TokenTypes.FINAL) != null) {
300                     constantDef = varDefAST;
301                 }
302             }
303         }
304         return constantDef;
305     }
306 
307     /**
308      * Reports aAST as a magic number, includes unary operators as needed.
309      *
310      * @param ast the AST node that contains the number to report
311      */
312     private void reportMagicNumber(DetailAST ast) {
313         String text = ast.getText();
314         final DetailAST parent = ast.getParent();
315         DetailAST reportAST = ast;
316         if (parent.getType() == TokenTypes.UNARY_MINUS) {
317             reportAST = parent;
318             text = "-" + text;
319         }
320         else if (parent.getType() == TokenTypes.UNARY_PLUS) {
321             reportAST = parent;
322             text = "+" + text;
323         }
324         log(reportAST,
325                 MSG_KEY,
326                 text);
327     }
328 
329     /**
330      * Determines whether or not the given AST is in a valid hash code method.
331      * A valid hash code method is considered to be a method of the signature
332      * {@code public int hashCode()}.
333      *
334      * @param ast the AST from which to search for an enclosing hash code
335      *     method definition
336      *
337      * @return {@code true} if {@code ast} is in the scope of a valid hash code method.
338      */
339     private static boolean isInHashCodeMethod(DetailAST ast) {
340         // find the method definition AST
341         DetailAST currentAST = ast;
342         while (currentAST != null
343                 && currentAST.getType() != TokenTypes.METHOD_DEF) {
344             currentAST = currentAST.getParent();
345         }
346         final DetailAST methodDefAST = currentAST;
347         boolean inHashCodeMethod = false;
348 
349         if (methodDefAST != null) {
350             // Check for 'hashCode' name.
351             final DetailAST identAST = methodDefAST.findFirstToken(TokenTypes.IDENT);
352 
353             if ("hashCode".equals(identAST.getText())) {
354                 // Check for no arguments.
355                 final DetailAST paramAST = methodDefAST.findFirstToken(TokenTypes.PARAMETERS);
356                 // we are in a 'public int hashCode()' method! The compiler will ensure
357                 // the method returns an 'int' and is public.
358                 inHashCodeMethod = !paramAST.hasChildren();
359             }
360         }
361         return inHashCodeMethod;
362     }
363 
364     /**
365      * Decides whether the number of an AST is in the ignore list of this
366      * check.
367      *
368      * @param ast the AST to check
369      * @return true if the number of ast is in the ignore list of this check.
370      */
371     private boolean isInIgnoreList(DetailAST ast) {
372         double value = CheckUtil.parseDouble(ast.getText(), ast.getType());
373         final DetailAST parent = ast.getParent();
374         if (parent.getType() == TokenTypes.UNARY_MINUS) {
375             value = -1 * value;
376         }
377         return Arrays.binarySearch(ignoreNumbers, value) >= 0;
378     }
379 
380     /**
381      * Determines whether or not the given AST is field declaration.
382      *
383      * @param ast AST from which to search for an enclosing field declaration
384      *
385      * @return {@code true} if {@code ast} is in the scope of field declaration
386      */
387     private static boolean isFieldDeclaration(DetailAST ast) {
388         DetailAST varDefAST = null;
389         DetailAST node = ast;
390         while (node.getType() != TokenTypes.OBJBLOCK) {
391             if (node.getType() == TokenTypes.VARIABLE_DEF) {
392                 varDefAST = node;
393                 break;
394             }
395             node = node.getParent();
396         }
397 
398         // contains variable declaration
399         // and it is directly inside class or record declaration
400         return varDefAST != null
401                 && (varDefAST.getParent().getParent().getType() == TokenTypes.CLASS_DEF
402                 || varDefAST.getParent().getParent().getType() == TokenTypes.RECORD_DEF
403                 || varDefAST.getParent().getParent().getType() == TokenTypes.LITERAL_NEW);
404     }
405 
406     /**
407      * Setter to specify tokens that are allowed in the AST path from the
408      * number literal to the enclosing constant definition.
409      *
410      * @param tokens The string representation of the tokens interested in
411      * @since 6.11
412      */
413     public void setConstantWaiverParentToken(String... tokens) {
414         constantWaiverParentToken = TokenUtil.asBitSet(tokens);
415     }
416 
417     /**
418      * Setter to specify non-magic numbers.
419      *
420      * @param list numbers to ignore.
421      * @since 3.1
422      */
423     public void setIgnoreNumbers(double... list) {
424         ignoreNumbers = new double[list.length];
425         System.arraycopy(list, 0, ignoreNumbers, 0, list.length);
426         Arrays.sort(ignoreNumbers);
427     }
428 
429     /**
430      * Setter to ignore magic numbers in hashCode methods.
431      *
432      * @param ignoreHashCodeMethod decide whether to ignore
433      *     hash code methods
434      * @since 5.3
435      */
436     public void setIgnoreHashCodeMethod(boolean ignoreHashCodeMethod) {
437         this.ignoreHashCodeMethod = ignoreHashCodeMethod;
438     }
439 
440     /**
441      * Setter to ignore magic numbers in annotation declarations.
442      *
443      * @param ignoreAnnotation decide whether to ignore annotations
444      * @since 5.4
445      */
446     public void setIgnoreAnnotation(boolean ignoreAnnotation) {
447         this.ignoreAnnotation = ignoreAnnotation;
448     }
449 
450     /**
451      * Setter to ignore magic numbers in field declarations.
452      *
453      * @param ignoreFieldDeclaration decide whether to ignore magic numbers
454      *     in field declaration
455      * @since 6.6
456      */
457     public void setIgnoreFieldDeclaration(boolean ignoreFieldDeclaration) {
458         this.ignoreFieldDeclaration = ignoreFieldDeclaration;
459     }
460 
461     /**
462      * Setter to ignore magic numbers in annotation elements defaults.
463      *
464      * @param ignoreAnnotationElementDefaults decide whether to ignore annotation elements defaults
465      * @since 8.23
466      */
467     public void setIgnoreAnnotationElementDefaults(boolean ignoreAnnotationElementDefaults) {
468         this.ignoreAnnotationElementDefaults = ignoreAnnotationElementDefaults;
469     }
470 
471     /**
472      * Determines if the given AST node has a parent node with given token type code.
473      *
474      * @param ast the AST from which to search for annotations
475      * @param type the type code of parent token
476      *
477      * @return {@code true} if the AST node has a parent with given token type.
478      */
479     private static boolean isChildOf(DetailAST ast, int type) {
480         boolean result = false;
481         DetailAST node = ast;
482         do {
483             if (node.getType() == type) {
484                 result = true;
485                 break;
486             }
487             node = node.getParent();
488         } while (node != null);
489 
490         return result;
491     }
492 
493 }