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.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   * <pre>
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   * </pre>
60   * <ul>
61   * <li>
62   * Property {@code ignoreAnnotatedBy} - Ignore classes annotated
63   * with the specified annotation(s). Annotation names provided in this property
64   * must exactly match the annotation names on the classes. If the target class has annotations
65   * specified with their fully qualified names (including package), the annotations in this
66   * property should also be specified with their fully qualified names. Similarly, if the target
67   * class has annotations specified with their simple names, this property should contain the
68   * annotations with the same simple names.
69   * Type is {@code java.lang.String[]}.
70   * Default value is {@code ""}.
71   * </li>
72   * </ul>
73   *
74   * <p>
75   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
76   * </p>
77   *
78   * <p>
79   * Violation Message Keys:
80   * </p>
81   * <ul>
82   * <li>
83   * {@code hide.utility.class}
84   * </li>
85   * </ul>
86   *
87   * @since 3.1
88   */
89  @StatelessCheck
90  public class HideUtilityClassConstructorCheck extends AbstractCheck {
91  
92      /**
93       * A key is pointing to the warning message text in "messages.properties"
94       * file.
95       */
96      public static final String MSG_KEY = "hide.utility.class";
97  
98      /**
99       * Ignore classes annotated with the specified annotation(s). Annotation names
100      * provided in this property must exactly match the annotation names on the classes.
101      * If the target class has annotations specified with their fully qualified names
102      * (including package), the annotations in this property should also be specified with
103      * their fully qualified names. Similarly, if the target class has annotations specified
104      * with their simple names, this property should contain the annotations with the same
105      * simple names.
106      */
107     private Set<String> ignoreAnnotatedBy = Collections.emptySet();
108 
109     /**
110      * Setter to ignore classes annotated with the specified annotation(s). Annotation names
111      * provided in this property must exactly match the annotation names on the classes.
112      * If the target class has annotations specified with their fully qualified names
113      * (including package), the annotations in this property should also be specified with
114      * their fully qualified names. Similarly, if the target class has annotations specified
115      * with their simple names, this property should contain the annotations with the same
116      * simple names.
117      *
118      * @param annotationNames specified annotation(s)
119      * @since 10.20.0
120      */
121     public void setIgnoreAnnotatedBy(String... annotationNames) {
122         ignoreAnnotatedBy = Set.of(annotationNames);
123     }
124 
125     @Override
126     public int[] getDefaultTokens() {
127         return getRequiredTokens();
128     }
129 
130     @Override
131     public int[] getAcceptableTokens() {
132         return getRequiredTokens();
133     }
134 
135     @Override
136     public int[] getRequiredTokens() {
137         return new int[] {TokenTypes.CLASS_DEF};
138     }
139 
140     @Override
141     public void visitToken(DetailAST ast) {
142         // abstract class could not have private constructor
143         if (!isAbstract(ast) && !shouldIgnoreClass(ast)) {
144             final boolean hasStaticModifier = isStatic(ast);
145 
146             final Details details = new Details(ast);
147             details.invoke();
148 
149             final boolean hasDefaultCtor = details.isHasDefaultCtor();
150             final boolean hasPublicCtor = details.isHasPublicCtor();
151             final boolean hasNonStaticMethodOrField = details.isHasNonStaticMethodOrField();
152             final boolean hasNonPrivateStaticMethodOrField =
153                     details.isHasNonPrivateStaticMethodOrField();
154 
155             final boolean hasAccessibleCtor = hasDefaultCtor || hasPublicCtor;
156 
157             // figure out if class extends java.lang.object directly
158             // keep it simple for now and get a 99% solution
159             final boolean extendsJlo =
160                 ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE) == null;
161 
162             final boolean isUtilClass = extendsJlo
163                 && !hasNonStaticMethodOrField && hasNonPrivateStaticMethodOrField;
164 
165             if (isUtilClass && hasAccessibleCtor && !hasStaticModifier) {
166                 log(ast, MSG_KEY);
167             }
168         }
169     }
170 
171     /**
172      * Returns true if given class is abstract or false.
173      *
174      * @param ast class definition for check.
175      * @return true if a given class declared as abstract.
176      */
177     private static boolean isAbstract(DetailAST ast) {
178         return ast.findFirstToken(TokenTypes.MODIFIERS)
179             .findFirstToken(TokenTypes.ABSTRACT) != null;
180     }
181 
182     /**
183      * Returns true if given class is static or false.
184      *
185      * @param ast class definition for check.
186      * @return true if a given class declared as static.
187      */
188     private static boolean isStatic(DetailAST ast) {
189         return ast.findFirstToken(TokenTypes.MODIFIERS)
190             .findFirstToken(TokenTypes.LITERAL_STATIC) != null;
191     }
192 
193     /**
194      * Checks if class is annotated by specific annotation(s) to skip.
195      *
196      * @param ast class to check
197      * @return true if annotated by ignored annotations
198      */
199     private boolean shouldIgnoreClass(DetailAST ast) {
200         return AnnotationUtil.containsAnnotation(ast, ignoreAnnotatedBy);
201     }
202 
203     /**
204      * Details of class that are required for validation.
205      */
206     private static final class Details {
207 
208         /** Class ast. */
209         private final DetailAST ast;
210         /** Result of details gathering. */
211         private boolean hasNonStaticMethodOrField;
212         /** Result of details gathering. */
213         private boolean hasNonPrivateStaticMethodOrField;
214         /** Result of details gathering. */
215         private boolean hasDefaultCtor;
216         /** Result of details gathering. */
217         private boolean hasPublicCtor;
218 
219         /**
220          * C-tor.
221          *
222          * @param ast class ast
223          */
224         private Details(DetailAST ast) {
225             this.ast = ast;
226         }
227 
228         /**
229          * Getter.
230          *
231          * @return boolean
232          */
233         public boolean isHasNonStaticMethodOrField() {
234             return hasNonStaticMethodOrField;
235         }
236 
237         /**
238          * Getter.
239          *
240          * @return boolean
241          */
242         public boolean isHasNonPrivateStaticMethodOrField() {
243             return hasNonPrivateStaticMethodOrField;
244         }
245 
246         /**
247          * Getter.
248          *
249          * @return boolean
250          */
251         public boolean isHasDefaultCtor() {
252             return hasDefaultCtor;
253         }
254 
255         /**
256          * Getter.
257          *
258          * @return boolean
259          */
260         public boolean isHasPublicCtor() {
261             return hasPublicCtor;
262         }
263 
264         /**
265          * Main method to gather statistics.
266          */
267         public void invoke() {
268             final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
269             hasDefaultCtor = true;
270             DetailAST child = objBlock.getFirstChild();
271 
272             while (child != null) {
273                 final int type = child.getType();
274                 if (type == TokenTypes.METHOD_DEF
275                         || type == TokenTypes.VARIABLE_DEF) {
276                     final DetailAST modifiers =
277                         child.findFirstToken(TokenTypes.MODIFIERS);
278                     final boolean isStatic =
279                         modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
280 
281                     if (isStatic) {
282                         final boolean isPrivate =
283                                 modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null;
284 
285                         if (!isPrivate) {
286                             hasNonPrivateStaticMethodOrField = true;
287                         }
288                     }
289                     else {
290                         hasNonStaticMethodOrField = true;
291                     }
292                 }
293                 if (type == TokenTypes.CTOR_DEF) {
294                     hasDefaultCtor = false;
295                     final DetailAST modifiers =
296                         child.findFirstToken(TokenTypes.MODIFIERS);
297                     if (modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null
298                         && modifiers.findFirstToken(TokenTypes.LITERAL_PROTECTED) == null) {
299                         // treat package visible as public
300                         // for the purpose of this Check
301                         hasPublicCtor = true;
302                     }
303                 }
304                 child = child.getNextSibling();
305             }
306         }
307 
308     }
309 
310 }