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