View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.checkstyle;
6   
7   import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
8   import com.puppycrawl.tools.checkstyle.api.DetailAST;
9   import com.puppycrawl.tools.checkstyle.api.TokenTypes;
10  import java.util.Set;
11  import java.util.regex.Pattern;
12  
13  /**
14   * Checks that a private static constant is mentioned more than once.
15   *
16   * <p>A {@code private static final} field that the file mentions only
17   * once is an indirection that pays for nothing: the reader has to jump
18   * to the top of the class in order to learn what the single expression
19   * below really says. Such a constant belongs inside its only usage,
20   * since a constant starts to pay for itself at the second usage, where
21   * it removes a duplicate.</p>
22   *
23   * <p>Usages are counted as identifiers of the very same text anywhere
24   * in the file, the declaration of the constant included, which is why a
25   * constant used exactly once is mentioned exactly twice. A name that
26   * some other declaration happens to reuse is counted too, which only
27   * makes the check more forgiving.</p>
28   *
29   * <p>Only a constant with a literal initializer is inspected. A
30   * constant built by a method call or by {@code new}, like a compiled
31   * {@link java.util.regex.Pattern}, stays where it is, since inlining it
32   * would move a computation into a method body and could turn a
33   * once-per-class cost into a per-call one. The same goes for an array,
34   * which is a mutable object no matter how final its field is.</p>
35   *
36   * <p>A constant that a Javadoc comment refers to, the way {@code {@value
37   * #NAME}} and {@code {@link #NAME}} do, is left alone as well, since
38   * the documentation is a usage of its own and inlining the constant
39   * would break it. So is {@code serialVersionUID}, which belongs to Java
40   * serialization rather than to the class that declares it.</p>
41   *
42   * <p>A constant that is not mentioned anywhere at all belongs to
43   * {@link ConstantUsageCheck}, which reports it as unused.</p>
44   *
45   * @since 0.73.4
46   */
47  public final class SingleUseConstantCheck extends AbstractCheck {
48  
49      /**
50       * Token types an inlineable initializer may be made of.
51       */
52      private static final Set<Integer> LITERALS = Set.of(
53          TokenTypes.EXPR,
54          TokenTypes.NUM_INT,
55          TokenTypes.NUM_LONG,
56          TokenTypes.NUM_FLOAT,
57          TokenTypes.NUM_DOUBLE,
58          TokenTypes.STRING_LITERAL,
59          TokenTypes.CHAR_LITERAL,
60          TokenTypes.TEXT_BLOCK_LITERAL_BEGIN,
61          TokenTypes.TEXT_BLOCK_CONTENT,
62          TokenTypes.TEXT_BLOCK_LITERAL_END,
63          TokenTypes.LITERAL_TRUE,
64          TokenTypes.LITERAL_FALSE,
65          TokenTypes.LITERAL_NULL,
66          TokenTypes.IDENT,
67          TokenTypes.DOT,
68          TokenTypes.LPAREN,
69          TokenTypes.RPAREN,
70          TokenTypes.UNARY_MINUS,
71          TokenTypes.UNARY_PLUS,
72          TokenTypes.PLUS,
73          TokenTypes.MINUS,
74          TokenTypes.STAR,
75          TokenTypes.DIV,
76          TokenTypes.MOD
77      );
78  
79      /**
80       * Default constructor.
81       */
82      public SingleUseConstantCheck() {
83          // nothing to initialize
84      }
85  
86      @Override
87      public int[] getDefaultTokens() {
88          return new int[]{TokenTypes.VARIABLE_DEF};
89      }
90  
91      @Override
92      public int[] getAcceptableTokens() {
93          return this.getDefaultTokens();
94      }
95  
96      @Override
97      public int[] getRequiredTokens() {
98          return this.getDefaultTokens();
99      }
100 
101     @Override
102     public void visitToken(final DetailAST ast) {
103         final DetailAST name = ast.findFirstToken(TokenTypes.IDENT);
104         if (SingleUseConstantCheck.isInlineable(ast)
105             && !this.documented(name.getText())
106             && SingleUseConstantCheck.mentions(
107                 SingleUseConstantCheck.root(ast), name.getText()
108             ) == 2) {
109             this.log(
110                 name.getLineNo(),
111                 String.format(
112                     "Private constant \"%s\" is used only once, inline it",
113                     name.getText()
114                 )
115             );
116         }
117     }
118 
119     private boolean documented(final String name) {
120         final Pattern ref = Pattern.compile(
121             String.format("#%s\\b", Pattern.quote(name))
122         );
123         boolean found = false;
124         for (final String line : this.getLines()) {
125             if (ref.matcher(line).find()) {
126                 found = true;
127                 break;
128             }
129         }
130         return found;
131     }
132 
133     private static boolean isInlineable(final DetailAST node) {
134         final DetailAST assign = node.findFirstToken(TokenTypes.ASSIGN);
135         return SingleUseConstantCheck.isConstant(node)
136             && assign != null
137             && SingleUseConstantCheck.isLiteral(assign.getFirstChild());
138     }
139 
140     private static boolean isConstant(final DetailAST node) {
141         return node.getParent().getType() == TokenTypes.OBJBLOCK
142             && !"serialVersionUID".equals(
143                 node.findFirstToken(TokenTypes.IDENT).getText()
144             )
145             && SingleUseConstantCheck.isShared(node);
146     }
147 
148     private static boolean isShared(final DetailAST node) {
149         final DetailAST mods = node.findFirstToken(TokenTypes.MODIFIERS);
150         return mods.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null
151             && mods.findFirstToken(TokenTypes.LITERAL_STATIC) != null
152             && mods.findFirstToken(TokenTypes.FINAL) != null;
153     }
154 
155     private static boolean isLiteral(final DetailAST node) {
156         boolean literal =
157             SingleUseConstantCheck.LITERALS.contains(node.getType());
158         DetailAST child = node.getFirstChild();
159         while (literal && child != null) {
160             literal = SingleUseConstantCheck.isLiteral(child);
161             child = child.getNextSibling();
162         }
163         return literal;
164     }
165 
166     private static int mentions(final DetailAST node, final String name) {
167         int found = 0;
168         if (node.getType() == TokenTypes.IDENT
169             && node.getText().equals(name)) {
170             found = 1;
171         }
172         DetailAST child = node.getFirstChild();
173         while (child != null) {
174             found += SingleUseConstantCheck.mentions(child, name);
175             child = child.getNextSibling();
176         }
177         return found;
178     }
179 
180     private static DetailAST root(final DetailAST node) {
181         DetailAST top = node;
182         while (top.getParent() != null) {
183             top = top.getParent();
184         }
185         return top;
186     }
187 }