View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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   * <div>
38   * Checks that there are no
39   * <a href="https://en.wikipedia.org/wiki/Magic_number_%28programming%29">
40   * {@code "magic number"}</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   * </div>
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   * {@snippet lang="text" :
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   * }
56   *
57   * @since 3.1
58   */
59  @StatelessCheck
60  public class MagicNumberCheck extends AbstractCheck {
61  
62      /**
63       * A key is pointing to the warning message text in "messages.properties"
64       * file.
65       */
66      public static final String MSG_KEY = "magic.number";
67  
68      /**
69       * Specify tokens that are allowed in the AST path from the
70       * number literal to the enclosing constant definition.
71       */
72      @XdocsPropertyType(PropertyType.TOKEN_ARRAY)
73      private BitSet constantWaiverParentToken = TokenUtil.asBitSet(
74          TokenTypes.ASSIGN,
75          TokenTypes.ARRAY_INIT,
76          TokenTypes.EXPR,
77          TokenTypes.UNARY_PLUS,
78          TokenTypes.UNARY_MINUS,
79          TokenTypes.TYPECAST,
80          TokenTypes.ELIST,
81          TokenTypes.LITERAL_NEW,
82          TokenTypes.METHOD_CALL,
83          TokenTypes.STAR,
84          TokenTypes.DIV,
85          TokenTypes.PLUS,
86          TokenTypes.MINUS,
87          TokenTypes.QUESTION,
88          TokenTypes.COLON,
89          TokenTypes.EQUAL,
90          TokenTypes.NOT_EQUAL,
91          TokenTypes.MOD,
92          TokenTypes.SR,
93          TokenTypes.BSR,
94          TokenTypes.GE,
95          TokenTypes.GT,
96          TokenTypes.SL,
97          TokenTypes.LE,
98          TokenTypes.LT,
99          TokenTypes.BXOR,
100         TokenTypes.BOR,
101         TokenTypes.BNOT,
102         TokenTypes.BAND
103     );
104 
105     /** Specify non-magic numbers. */
106     private double[] ignoreNumbers = {-1, 0, 1, 2};
107 
108     /** Ignore magic numbers in hashCode methods. */
109     private boolean ignoreHashCodeMethod;
110 
111     /** Ignore magic numbers in annotation declarations. */
112     private boolean ignoreAnnotation;
113 
114     /** Ignore magic numbers in field declarations. */
115     private boolean ignoreFieldDeclaration;
116 
117     /** Ignore magic numbers in annotation elements defaults. */
118     private boolean ignoreAnnotationElementDefaults = true;
119 
120     /**
121      * Creates a new {@code MagicNumberCheck} instance.
122      */
123     public MagicNumberCheck() {
124         // no code by default
125     }
126 
127     @Override
128     public int[] getDefaultTokens() {
129         return getAcceptableTokens();
130     }
131 
132     @Override
133     public int[] getAcceptableTokens() {
134         return new int[] {
135             TokenTypes.NUM_DOUBLE,
136             TokenTypes.NUM_FLOAT,
137             TokenTypes.NUM_INT,
138             TokenTypes.NUM_LONG,
139         };
140     }
141 
142     @Override
143     public int[] getRequiredTokens() {
144         return CommonUtil.EMPTY_INT_ARRAY;
145     }
146 
147     @Override
148     public void visitToken(DetailAST ast) {
149         if (shouldTestAnnotationArgs(ast)
150                 && shouldTestAnnotationDefaults(ast)
151                 && !isInIgnoreList(ast)
152                 && shouldCheckHashCodeMethod(ast)
153                 && shouldCheckFieldDeclaration(ast)) {
154             final DetailAST constantDefAST = findContainingConstantDef(ast);
155             if (isMagicNumberExists(ast, constantDefAST)) {
156                 reportMagicNumber(ast);
157             }
158         }
159     }
160 
161     /**
162      * Checks if ast is annotation argument and should be checked.
163      *
164      * @param ast token to check
165      * @return true if element is skipped, false otherwise
166      */
167     private boolean shouldTestAnnotationArgs(DetailAST ast) {
168         return !ignoreAnnotation || !isChildOf(ast, TokenTypes.ANNOTATION);
169     }
170 
171     /**
172      * Checks if ast is annotation element default value and should be checked.
173      *
174      * @param ast token to check
175      * @return true if element is skipped, false otherwise
176      */
177     private boolean shouldTestAnnotationDefaults(DetailAST ast) {
178         return !ignoreAnnotationElementDefaults || !isChildOf(ast, TokenTypes.LITERAL_DEFAULT);
179     }
180 
181     /**
182      * Checks if the given AST node is a HashCode Method and should be checked.
183      *
184      * @param ast the AST node to check
185      * @return true if element should be checked, false otherwise
186      */
187     private boolean shouldCheckHashCodeMethod(DetailAST ast) {
188         return !ignoreHashCodeMethod || !isInHashCodeMethod(ast);
189     }
190 
191     /**
192      * Checks if the given AST node is a field declaration and should be checked.
193      *
194      * @param ast the AST node to check
195      * @return true if element should be checked, false otherwise
196      */
197     private boolean shouldCheckFieldDeclaration(DetailAST ast) {
198         return !ignoreFieldDeclaration || !isFieldDeclaration(ast);
199     }
200 
201     /**
202      * Is magic number somewhere at ast tree.
203      *
204      * @param ast ast token
205      * @param constantDefAST constant ast
206      * @return true if magic number is present
207      */
208     private boolean isMagicNumberExists(DetailAST ast, DetailAST constantDefAST) {
209         boolean found = false;
210         DetailAST astNode = ast.getParent();
211         while (astNode != constantDefAST) {
212             final int type = astNode.getType();
213 
214             if (!constantWaiverParentToken.get(type)) {
215                 found = true;
216                 break;
217             }
218 
219             astNode = astNode.getParent();
220         }
221         return found;
222     }
223 
224     /**
225      * Finds the constant definition that contains aAST.
226      *
227      * @param ast the AST
228      * @return the constant def or null if ast is not contained in a constant definition.
229      */
230     private static DetailAST findContainingConstantDef(DetailAST ast) {
231         DetailAST varDefAST = ast;
232         while (varDefAST != null
233                 && varDefAST.getType() != TokenTypes.VARIABLE_DEF
234                 && varDefAST.getType() != TokenTypes.ENUM_CONSTANT_DEF) {
235             varDefAST = varDefAST.getParent();
236         }
237         DetailAST constantDef = null;
238 
239         // no containing variable definition?
240         if (varDefAST != null) {
241             // implicit constant?
242             if (ScopeUtil.isInInterfaceOrAnnotationBlock(varDefAST)
243                     || varDefAST.getType() == TokenTypes.ENUM_CONSTANT_DEF) {
244                 constantDef = varDefAST;
245             }
246             else {
247                 // explicit constant
248                 final DetailAST modifiersAST = varDefAST.findFirstToken(TokenTypes.MODIFIERS);
249 
250                 if (modifiersAST.findFirstToken(TokenTypes.FINAL) != null) {
251                     constantDef = varDefAST;
252                 }
253             }
254         }
255         return constantDef;
256     }
257 
258     /**
259      * Reports aAST as a magic number, includes unary operators as needed.
260      *
261      * @param ast the AST node that contains the number to report
262      */
263     private void reportMagicNumber(DetailAST ast) {
264         String text = ast.getText();
265         final DetailAST parent = ast.getParent();
266         DetailAST reportAST = ast;
267         if (parent.getType() == TokenTypes.UNARY_MINUS) {
268             reportAST = parent;
269             text = "-" + text;
270         }
271         else if (parent.getType() == TokenTypes.UNARY_PLUS) {
272             reportAST = parent;
273             text = "+" + text;
274         }
275         log(reportAST,
276                 MSG_KEY,
277                 text);
278     }
279 
280     /**
281      * Determines whether or not the given AST is in a valid hash code method.
282      * A valid hash code method is considered to be a method of the signature
283      * {@code public int hashCode()}.
284      *
285      * @param ast the AST from which to search for an enclosing hash code
286      *     method definition
287      *
288      * @return {@code true} if {@code ast} is in the scope of a valid hash code method.
289      */
290     private static boolean isInHashCodeMethod(DetailAST ast) {
291         // find the method definition AST
292         DetailAST currentAST = ast;
293         while (currentAST != null
294                 && currentAST.getType() != TokenTypes.METHOD_DEF) {
295             currentAST = currentAST.getParent();
296         }
297         final DetailAST methodDefAST = currentAST;
298         boolean inHashCodeMethod = false;
299 
300         if (methodDefAST != null) {
301             // Check for 'hashCode' name.
302             final DetailAST identAST = methodDefAST.findFirstToken(TokenTypes.IDENT);
303 
304             if ("hashCode".equals(identAST.getText())) {
305                 // Check for no arguments.
306                 final DetailAST paramAST = methodDefAST.findFirstToken(TokenTypes.PARAMETERS);
307                 // we are in a 'public int hashCode()' method! The compiler will ensure
308                 // the method returns an 'int' and is public.
309                 inHashCodeMethod = !paramAST.hasChildren();
310             }
311         }
312         return inHashCodeMethod;
313     }
314 
315     /**
316      * Decides whether the number of an AST is in the ignore list of this
317      * check.
318      *
319      * @param ast the AST to check
320      * @return true if the number of ast is in the ignore list of this check.
321      */
322     private boolean isInIgnoreList(DetailAST ast) {
323         double value = CheckUtil.parseDouble(ast.getText(), ast.getType());
324         final DetailAST parent = ast.getParent();
325         if (parent.getType() == TokenTypes.UNARY_MINUS) {
326             value = -1 * value;
327         }
328         return Arrays.binarySearch(ignoreNumbers, value) >= 0;
329     }
330 
331     /**
332      * Determines whether or not the given AST is field declaration.
333      *
334      * @param ast AST from which to search for an enclosing field declaration
335      *
336      * @return {@code true} if {@code ast} is in the scope of field declaration
337      */
338     private static boolean isFieldDeclaration(DetailAST ast) {
339         DetailAST varDefAST = null;
340         DetailAST node = ast;
341         while (node != null && node.getType() != TokenTypes.OBJBLOCK) {
342             if (node.getType() == TokenTypes.VARIABLE_DEF) {
343                 varDefAST = node;
344                 break;
345             }
346             node = node.getParent();
347         }
348 
349         boolean result = false;
350 
351         if (varDefAST != null) {
352             final DetailAST parent = varDefAST.getParent();
353 
354             if (parent.getType() == TokenTypes.COMPACT_COMPILATION_UNIT) {
355                 result = true;
356             }
357             else {
358                 final DetailAST grandParent = parent.getParent();
359 
360                 result = grandParent.getType() == TokenTypes.CLASS_DEF
361                     || grandParent.getType() == TokenTypes.RECORD_DEF
362                     || grandParent.getType() == TokenTypes.LITERAL_NEW;
363             }
364         }
365 
366         return result;
367     }
368 
369     /**
370      * Setter to specify tokens that are allowed in the AST path from the
371      * number literal to the enclosing constant definition.
372      *
373      * @param tokens The string representation of the tokens interested in
374      * @since 6.11
375      */
376     public void setConstantWaiverParentToken(String... tokens) {
377         constantWaiverParentToken = TokenUtil.asBitSet(tokens);
378     }
379 
380     /**
381      * Setter to specify non-magic numbers.
382      *
383      * @param list numbers to ignore.
384      * @since 3.1
385      */
386     public void setIgnoreNumbers(double... list) {
387         ignoreNumbers = new double[list.length];
388         System.arraycopy(list, 0, ignoreNumbers, 0, list.length);
389         Arrays.sort(ignoreNumbers);
390     }
391 
392     /**
393      * Setter to ignore magic numbers in hashCode methods.
394      *
395      * @param ignoreHashCodeMethod decide whether to ignore
396      *     hash code methods
397      * @since 5.3
398      */
399     public void setIgnoreHashCodeMethod(boolean ignoreHashCodeMethod) {
400         this.ignoreHashCodeMethod = ignoreHashCodeMethod;
401     }
402 
403     /**
404      * Setter to ignore magic numbers in annotation declarations.
405      *
406      * @param ignoreAnnotation decide whether to ignore annotations
407      * @since 5.4
408      */
409     public void setIgnoreAnnotation(boolean ignoreAnnotation) {
410         this.ignoreAnnotation = ignoreAnnotation;
411     }
412 
413     /**
414      * Setter to ignore magic numbers in field declarations.
415      *
416      * @param ignoreFieldDeclaration decide whether to ignore magic numbers
417      *     in field declaration
418      * @since 6.6
419      */
420     public void setIgnoreFieldDeclaration(boolean ignoreFieldDeclaration) {
421         this.ignoreFieldDeclaration = ignoreFieldDeclaration;
422     }
423 
424     /**
425      * Setter to ignore magic numbers in annotation elements defaults.
426      *
427      * @param ignoreAnnotationElementDefaults decide whether to ignore annotation elements defaults
428      * @since 8.23
429      */
430     public void setIgnoreAnnotationElementDefaults(boolean ignoreAnnotationElementDefaults) {
431         this.ignoreAnnotationElementDefaults = ignoreAnnotationElementDefaults;
432     }
433 
434     /**
435      * Determines if the given AST node has a parent node with given token type code.
436      *
437      * @param ast the AST from which to search for annotations
438      * @param type the type code of parent token
439      *
440      * @return {@code true} if the AST node has a parent with given token type.
441      */
442     private static boolean isChildOf(DetailAST ast, int type) {
443         boolean result = false;
444         DetailAST node = ast;
445         do {
446             if (node.getType() == type) {
447                 result = true;
448                 break;
449             }
450             node = node.getParent();
451         } while (node != null);
452 
453         return result;
454     }
455 
456 }