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 * {@snippet :
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 * }
059 *
060 * @since 3.1
061 * @noinspection EmptyClass, NonFinalUtilityClass, UtilityClassWithoutPrivateConstructor,
062 *      NewExceptionWithoutArguments
063 * @noinspectionreason EmptyClass - until IDEA-394242
064 * @noinspectionreason NonFinalUtilityClass - until IDEA-394413
065 * @noinspectionreason UtilityClassWithoutPrivateConstructor - until IDEA-394416
066 * @noinspectionreason NewExceptionWithoutArguments - until IDEA-394415
067 */
068@StatelessCheck
069public class HideUtilityClassConstructorCheck extends AbstractCheck {
070
071    /**
072     * A key is pointing to the warning message text in "messages.properties" file.
073     */
074    public static final String MSG_KEY = "hide.utility.class";
075
076    /**
077     * Ignore classes annotated with the specified annotation(s). Annotation names
078     * provided in this property must exactly match the annotation names on the classes.
079     * If the target class has annotations specified with their fully qualified names
080     * (including package), the annotations in this property should also be specified with
081     * their fully qualified names. Similarly, if the target class has annotations specified
082     * with their simple names, this property should contain the annotations with the same
083     * simple names.
084     */
085    private Set<String> ignoreAnnotatedBy = Set.of();
086
087    /**
088     * Creates a new {@code HideUtilityClassConstructorCheck} instance.
089     */
090    public HideUtilityClassConstructorCheck() {
091        // no code by default
092    }
093
094    /**
095     * Setter to ignore classes annotated with the specified annotation(s). Annotation names
096     * provided in this property must exactly match the annotation names on the classes.
097     * If the target class has annotations specified with their fully qualified names
098     * (including package), the annotations in this property should also be specified with
099     * their fully qualified names. Similarly, if the target class has annotations specified
100     * with their simple names, this property should contain the annotations with the same
101     * simple names.
102     *
103     * @param annotationNames specified annotation(s)
104     * @since 10.20.0
105     */
106    public void setIgnoreAnnotatedBy(String... annotationNames) {
107        ignoreAnnotatedBy = Set.of(annotationNames);
108    }
109
110    @Override
111    public int[] getDefaultTokens() {
112        return getRequiredTokens();
113    }
114
115    @Override
116    public int[] getAcceptableTokens() {
117        return getRequiredTokens();
118    }
119
120    @Override
121    public int[] getRequiredTokens() {
122        return new int[] {TokenTypes.CLASS_DEF};
123    }
124
125    @Override
126    public void visitToken(DetailAST ast) {
127        // abstract class could not have private constructor
128        if (!isAbstract(ast) && !shouldIgnoreClass(ast)) {
129            final boolean hasStaticModifier = isStatic(ast);
130
131            final Details details = new Details(ast);
132            details.invoke();
133
134            final boolean hasDefaultCtor = details.isHasDefaultCtor();
135            final boolean hasPublicCtor = details.isHasPublicCtor();
136            final boolean hasNonStaticMethodOrField = details.isHasNonStaticMethodOrField();
137            final boolean hasNonPrivateStaticMethodOrField =
138                    details.isHasNonPrivateStaticMethodOrField();
139
140            final boolean hasAccessibleCtor = hasDefaultCtor || hasPublicCtor;
141
142            // figure out if class extends java.lang.object directly
143            // keep it simple for now and get a 99% solution
144            final boolean extendsJlo =
145                ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE) == null;
146
147            final boolean isUtilClass = extendsJlo
148                && !hasNonStaticMethodOrField && hasNonPrivateStaticMethodOrField;
149
150            if (isUtilClass && hasAccessibleCtor && !hasStaticModifier) {
151                log(ast, MSG_KEY);
152            }
153        }
154    }
155
156    /**
157     * Returns true if given class is abstract or false.
158     *
159     * @param ast class definition for check.
160     * @return true if a given class declared as abstract.
161     */
162    private static boolean isAbstract(DetailAST ast) {
163        return ast.findFirstToken(TokenTypes.MODIFIERS)
164            .findFirstToken(TokenTypes.ABSTRACT) != null;
165    }
166
167    /**
168     * Returns true if given class is static or false.
169     *
170     * @param ast class definition for check.
171     * @return true if a given class declared as static.
172     */
173    private static boolean isStatic(DetailAST ast) {
174        return ast.findFirstToken(TokenTypes.MODIFIERS)
175            .findFirstToken(TokenTypes.LITERAL_STATIC) != null;
176    }
177
178    /**
179     * Checks if class is annotated by specific annotation(s) to skip.
180     *
181     * @param ast class to check
182     * @return true if annotated by ignored annotations
183     */
184    private boolean shouldIgnoreClass(DetailAST ast) {
185        return AnnotationUtil.containsAnnotation(ast, ignoreAnnotatedBy);
186    }
187
188    /**
189     * Details of class that are required for validation.
190     */
191    private static final class Details {
192
193        /** Class ast. */
194        private final DetailAST ast;
195        /** Result of details gathering. */
196        private boolean hasNonStaticMethodOrField;
197        /** Result of details gathering. */
198        private boolean hasNonPrivateStaticMethodOrField;
199        /** Result of details gathering. */
200        private boolean hasDefaultCtor;
201        /** Result of details gathering. */
202        private boolean hasPublicCtor;
203
204        /**
205         * C-tor.
206         *
207         * @param ast class ast
208         */
209        private Details(DetailAST ast) {
210            this.ast = ast;
211        }
212
213        /**
214         * Getter.
215         *
216         * @return boolean
217         */
218        /* package */ boolean isHasNonStaticMethodOrField() {
219            return hasNonStaticMethodOrField;
220        }
221
222        /**
223         * Getter.
224         *
225         * @return boolean
226         */
227        /* package */ boolean isHasNonPrivateStaticMethodOrField() {
228            return hasNonPrivateStaticMethodOrField;
229        }
230
231        /**
232         * Getter.
233         *
234         * @return boolean
235         */
236        /* package */ boolean isHasDefaultCtor() {
237            return hasDefaultCtor;
238        }
239
240        /**
241         * Getter.
242         *
243         * @return boolean
244         */
245        /* package */ boolean isHasPublicCtor() {
246            return hasPublicCtor;
247        }
248
249        /**
250         * Main method to gather statistics.
251         */
252        /* package */ void invoke() {
253            final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
254            hasDefaultCtor = true;
255            DetailAST child = objBlock.getFirstChild();
256
257            while (child != null) {
258                final int type = child.getType();
259                if (type == TokenTypes.METHOD_DEF
260                        || type == TokenTypes.VARIABLE_DEF) {
261                    final DetailAST modifiers =
262                        child.findFirstToken(TokenTypes.MODIFIERS);
263                    final boolean isStatic =
264                        modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
265
266                    if (isStatic) {
267                        final boolean isPrivate =
268                                modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null;
269
270                        if (!isPrivate) {
271                            hasNonPrivateStaticMethodOrField = true;
272                        }
273                    }
274                    else {
275                        hasNonStaticMethodOrField = true;
276                    }
277                }
278                if (type == TokenTypes.CTOR_DEF) {
279                    hasDefaultCtor = false;
280                    final DetailAST modifiers =
281                        child.findFirstToken(TokenTypes.MODIFIERS);
282                    if (modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null
283                        && modifiers.findFirstToken(TokenTypes.LITERAL_PROTECTED) == null) {
284                        // treat package visible as public
285                        // for the purpose of this Check
286                        hasPublicCtor = true;
287                    }
288                }
289                child = child.getNextSibling();
290            }
291        }
292
293    }
294
295}