001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.design;
021
022import java.util.Set;
023
024import com.puppycrawl.tools.checkstyle.StatelessCheck;
025import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailAST;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
029
030/**
031 * <div>
032 * Makes sure that utility classes (classes that contain only static methods or fields in their API)
033 * do not have a public constructor.
034 * </div>
035 *
036 * <p>
037 * Rationale: Instantiating utility classes does not make sense.
038 * Hence, the constructors should either be private or (if you want to allow subclassing) protected.
039 * A common mistake is forgetting to hide the default constructor.
040 * </p>
041 *
042 * <p>
043 * If you make the constructor protected you may want to consider the following constructor
044 * implementation technique to disallow instantiating subclasses:
045 * </p>
046 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
047 * public class StringUtils // not final to allow subclassing
048 * {
049 *   protected StringUtils() {
050 *     // prevents calls from subclass
051 *     throw new UnsupportedOperationException();
052 *   }
053 *
054 *   public static int count(char c, String s) {
055 *     // ...
056 *   }
057 * }
058 * </code></pre></div>
059 *
060 * @since 3.1
061 */
062@StatelessCheck
063public class HideUtilityClassConstructorCheck extends AbstractCheck {
064
065    /**
066     * A key is pointing to the warning message text in "messages.properties"
067     * file.
068     */
069    public static final String MSG_KEY = "hide.utility.class";
070
071    /**
072     * Ignore classes annotated with the specified annotation(s). Annotation names
073     * provided in this property must exactly match the annotation names on the classes.
074     * If the target class has annotations specified with their fully qualified names
075     * (including package), the annotations in this property should also be specified with
076     * their fully qualified names. Similarly, if the target class has annotations specified
077     * with their simple names, this property should contain the annotations with the same
078     * simple names.
079     */
080    private Set<String> ignoreAnnotatedBy = Set.of();
081
082    /**
083     * Creates a new {@code HideUtilityClassConstructorCheck} instance.
084     */
085    public HideUtilityClassConstructorCheck() {
086        // no code by default
087    }
088
089    /**
090     * Setter to ignore classes annotated with the specified annotation(s). Annotation names
091     * provided in this property must exactly match the annotation names on the classes.
092     * If the target class has annotations specified with their fully qualified names
093     * (including package), the annotations in this property should also be specified with
094     * their fully qualified names. Similarly, if the target class has annotations specified
095     * with their simple names, this property should contain the annotations with the same
096     * simple names.
097     *
098     * @param annotationNames specified annotation(s)
099     * @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}