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.design;
21  
22  import java.util.Set;
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.AnnotationUtil;
29  
30  /**
31   * <div>
32   * Makes sure that utility classes (classes that contain only static methods or fields in their API)
33   * do not have a public constructor.
34   * </div>
35   *
36   * <p>
37   * Rationale: Instantiating utility classes does not make sense.
38   * Hence, the constructors should either be private or (if you want to allow subclassing) protected.
39   * A common mistake is forgetting to hide the default constructor.
40   * </p>
41   *
42   * <p>
43   * If you make the constructor protected you may want to consider the following constructor
44   * implementation technique to disallow instantiating subclasses:
45   * </p>
46   * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
47   * public class StringUtils // not final to allow subclassing
48   * {
49   *   protected StringUtils() {
50   *     // prevents calls from subclass
51   *     throw new UnsupportedOperationException();
52   *   }
53   *
54   *   public static int count(char c, String s) {
55   *     // ...
56   *   }
57   * }
58   * </code></pre></div>
59   *
60   * @since 3.1
61   */
62  @StatelessCheck
63  public class HideUtilityClassConstructorCheck extends AbstractCheck {
64  
65      /**
66       * A key is pointing to the warning message text in "messages.properties"
67       * file.
68       */
69      public static final String MSG_KEY = "hide.utility.class";
70  
71      /**
72       * Ignore classes annotated with the specified annotation(s). Annotation names
73       * provided in this property must exactly match the annotation names on the classes.
74       * If the target class has annotations specified with their fully qualified names
75       * (including package), the annotations in this property should also be specified with
76       * their fully qualified names. Similarly, if the target class has annotations specified
77       * with their simple names, this property should contain the annotations with the same
78       * simple names.
79       */
80      private Set<String> ignoreAnnotatedBy = Set.of();
81  
82      /**
83       * Creates a new {@code HideUtilityClassConstructorCheck} instance.
84       */
85      public HideUtilityClassConstructorCheck() {
86          // no code by default
87      }
88  
89      /**
90       * Setter to ignore classes annotated with the specified annotation(s). Annotation names
91       * provided in this property must exactly match the annotation names on the classes.
92       * If the target class has annotations specified with their fully qualified names
93       * (including package), the annotations in this property should also be specified with
94       * their fully qualified names. Similarly, if the target class has annotations specified
95       * with their simple names, this property should contain the annotations with the same
96       * simple names.
97       *
98       * @param annotationNames specified annotation(s)
99       * @since 10.20.0
100      */
101     public void setIgnoreAnnotatedBy(String... annotationNames) {
102         ignoreAnnotatedBy = Set.of(annotationNames);
103     }
104 
105     @Override
106     public int[] getDefaultTokens() {
107         return getRequiredTokens();
108     }
109 
110     @Override
111     public int[] getAcceptableTokens() {
112         return getRequiredTokens();
113     }
114 
115     @Override
116     public int[] getRequiredTokens() {
117         return new int[] {TokenTypes.CLASS_DEF};
118     }
119 
120     @Override
121     public void visitToken(DetailAST ast) {
122         // abstract class could not have private constructor
123         if (!isAbstract(ast) && !shouldIgnoreClass(ast)) {
124             final boolean hasStaticModifier = isStatic(ast);
125 
126             final Details details = new Details(ast);
127             details.invoke();
128 
129             final boolean hasDefaultCtor = details.isHasDefaultCtor();
130             final boolean hasPublicCtor = details.isHasPublicCtor();
131             final boolean hasNonStaticMethodOrField = details.isHasNonStaticMethodOrField();
132             final boolean hasNonPrivateStaticMethodOrField =
133                     details.isHasNonPrivateStaticMethodOrField();
134 
135             final boolean hasAccessibleCtor = hasDefaultCtor || hasPublicCtor;
136 
137             // figure out if class extends java.lang.object directly
138             // keep it simple for now and get a 99% solution
139             final boolean extendsJlo =
140                 ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE) == null;
141 
142             final boolean isUtilClass = extendsJlo
143                 && !hasNonStaticMethodOrField && hasNonPrivateStaticMethodOrField;
144 
145             if (isUtilClass && hasAccessibleCtor && !hasStaticModifier) {
146                 log(ast, MSG_KEY);
147             }
148         }
149     }
150 
151     /**
152      * Returns true if given class is abstract or false.
153      *
154      * @param ast class definition for check.
155      * @return true if a given class declared as abstract.
156      */
157     private static boolean isAbstract(DetailAST ast) {
158         return ast.findFirstToken(TokenTypes.MODIFIERS)
159             .findFirstToken(TokenTypes.ABSTRACT) != null;
160     }
161 
162     /**
163      * Returns true if given class is static or false.
164      *
165      * @param ast class definition for check.
166      * @return true if a given class declared as static.
167      */
168     private static boolean isStatic(DetailAST ast) {
169         return ast.findFirstToken(TokenTypes.MODIFIERS)
170             .findFirstToken(TokenTypes.LITERAL_STATIC) != null;
171     }
172 
173     /**
174      * Checks if class is annotated by specific annotation(s) to skip.
175      *
176      * @param ast class to check
177      * @return true if annotated by ignored annotations
178      */
179     private boolean shouldIgnoreClass(DetailAST ast) {
180         return AnnotationUtil.containsAnnotation(ast, ignoreAnnotatedBy);
181     }
182 
183     /**
184      * Details of class that are required for validation.
185      */
186     private static final class Details {
187 
188         /** Class ast. */
189         private final DetailAST ast;
190         /** Result of details gathering. */
191         private boolean hasNonStaticMethodOrField;
192         /** Result of details gathering. */
193         private boolean hasNonPrivateStaticMethodOrField;
194         /** Result of details gathering. */
195         private boolean hasDefaultCtor;
196         /** Result of details gathering. */
197         private boolean hasPublicCtor;
198 
199         /**
200          * C-tor.
201          *
202          * @param ast class ast
203          */
204         private Details(DetailAST ast) {
205             this.ast = ast;
206         }
207 
208         /**
209          * Getter.
210          *
211          * @return boolean
212          */
213         /* package */ boolean isHasNonStaticMethodOrField() {
214             return hasNonStaticMethodOrField;
215         }
216 
217         /**
218          * Getter.
219          *
220          * @return boolean
221          */
222         /* package */ boolean isHasNonPrivateStaticMethodOrField() {
223             return hasNonPrivateStaticMethodOrField;
224         }
225 
226         /**
227          * Getter.
228          *
229          * @return boolean
230          */
231         /* package */ boolean isHasDefaultCtor() {
232             return hasDefaultCtor;
233         }
234 
235         /**
236          * Getter.
237          *
238          * @return boolean
239          */
240         /* package */ boolean isHasPublicCtor() {
241             return hasPublicCtor;
242         }
243 
244         /**
245          * Main method to gather statistics.
246          */
247         /* package */ void invoke() {
248             final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
249             hasDefaultCtor = true;
250             DetailAST child = objBlock.getFirstChild();
251 
252             while (child != null) {
253                 final int type = child.getType();
254                 if (type == TokenTypes.METHOD_DEF
255                         || type == TokenTypes.VARIABLE_DEF) {
256                     final DetailAST modifiers =
257                         child.findFirstToken(TokenTypes.MODIFIERS);
258                     final boolean isStatic =
259                         modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
260 
261                     if (isStatic) {
262                         final boolean isPrivate =
263                                 modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null;
264 
265                         if (!isPrivate) {
266                             hasNonPrivateStaticMethodOrField = true;
267                         }
268                     }
269                     else {
270                         hasNonStaticMethodOrField = true;
271                     }
272                 }
273                 if (type == TokenTypes.CTOR_DEF) {
274                     hasDefaultCtor = false;
275                     final DetailAST modifiers =
276                         child.findFirstToken(TokenTypes.MODIFIERS);
277                     if (modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null
278                         && modifiers.findFirstToken(TokenTypes.LITERAL_PROTECTED) == null) {
279                         // treat package visible as public
280                         // for the purpose of this Check
281                         hasPublicCtor = true;
282                     }
283                 }
284                 child = child.getNextSibling();
285             }
286         }
287 
288     }
289 
290 }