001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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.Collections;
023import java.util.Set;
024
025import com.puppycrawl.tools.checkstyle.StatelessCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.TokenTypes;
029import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
030
031/**
032 * <div>
033 * Makes sure that utility classes (classes that contain only static methods or fields in their API)
034 * do not have a public constructor.
035 * </div>
036 *
037 * <p>
038 * Rationale: Instantiating utility classes does not make sense.
039 * Hence, the constructors should either be private or (if you want to allow subclassing) protected.
040 * A common mistake is forgetting to hide the default constructor.
041 * </p>
042 *
043 * <p>
044 * If you make the constructor protected you may want to consider the following constructor
045 * implementation technique to disallow instantiating subclasses:
046 * </p>
047 * <pre>
048 * public class StringUtils // not final to allow subclassing
049 * {
050 *   protected StringUtils() {
051 *     // prevents calls from subclass
052 *     throw new UnsupportedOperationException();
053 *   }
054 *
055 *   public static int count(char c, String s) {
056 *     // ...
057 *   }
058 * }
059 * </pre>
060 * <ul>
061 * <li>
062 * Property {@code ignoreAnnotatedBy} - Ignore classes annotated
063 * with the specified annotation(s). Annotation names provided in this property
064 * must exactly match the annotation names on the classes. If the target class has annotations
065 * specified with their fully qualified names (including package), the annotations in this
066 * property should also be specified with their fully qualified names. Similarly, if the target
067 * class has annotations specified with their simple names, this property should contain the
068 * annotations with the same simple names.
069 * Type is {@code java.lang.String[]}.
070 * Default value is {@code ""}.
071 * </li>
072 * </ul>
073 *
074 * <p>
075 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
076 * </p>
077 *
078 * <p>
079 * Violation Message Keys:
080 * </p>
081 * <ul>
082 * <li>
083 * {@code hide.utility.class}
084 * </li>
085 * </ul>
086 *
087 * @since 3.1
088 */
089@StatelessCheck
090public class HideUtilityClassConstructorCheck extends AbstractCheck {
091
092    /**
093     * A key is pointing to the warning message text in "messages.properties"
094     * file.
095     */
096    public static final String MSG_KEY = "hide.utility.class";
097
098    /**
099     * 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}