001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2022 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.ArrayDeque;
023import java.util.Deque;
024
025import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.FullIdent;
029import com.puppycrawl.tools.checkstyle.api.TokenTypes;
030import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
031
032/**
033 * <p>
034 * Checks that a class which has only private constructors
035 * is declared as final. Doesn't check for classes nested in interfaces
036 * or annotations, as they are always {@code final} there.
037 * </p>
038 * <p>
039 * To configure the check:
040 * </p>
041 * <pre>
042 * &lt;module name=&quot;FinalClass&quot;/&gt;
043 * </pre>
044 * <p>
045 * Example:
046 * </p>
047 * <pre>
048 * final class MyClass {  // OK
049 *   private MyClass() { }
050 * }
051 *
052 * class MyClass { // violation, class should be declared final
053 *   private MyClass() { }
054 * }
055 *
056 * class MyClass { // OK, since it has a public constructor
057 *   int field1;
058 *   String field2;
059 *   private MyClass(int value) {
060 *     this.field1 = value;
061 *     this.field2 = " ";
062 *   }
063 *   public MyClass(String value) {
064 *     this.field2 = value;
065 *     this.field1 = 0;
066 *   }
067 * }
068 *
069 * interface CheckInterface
070 * {
071 *   class MyClass { // OK, nested class in interface is always final
072 *     private MyClass() {}
073 *   }
074 * }
075 *
076 * public @interface Test {
077 *   public boolean enabled()
078 *   default true;
079 *   class MyClass { // OK, class nested in an annotation is always final
080 *     private MyClass() { }
081 *   }
082 * }
083 *
084 * class TestAnonymousInnerClasses { // OK, class has an anonymous inner class.
085 *     public static final TestAnonymousInnerClasses ONE = new TestAnonymousInnerClasses() {
086 *
087 *     };
088 *
089 *     private TestAnonymousInnerClasses() {
090 *     }
091 * }
092 * </pre>
093 * <p>
094 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
095 * </p>
096 * <p>
097 * Violation Message Keys:
098 * </p>
099 * <ul>
100 * <li>
101 * {@code final.class}
102 * </li>
103 * </ul>
104 *
105 * @since 3.1
106 */
107@FileStatefulCheck
108public class FinalClassCheck
109    extends AbstractCheck {
110
111    /**
112     * A key is pointing to the warning message text in "messages.properties"
113     * file.
114     */
115    public static final String MSG_KEY = "final.class";
116
117    /**
118     * Character separate package names in qualified name of java class.
119     */
120    private static final String PACKAGE_SEPARATOR = ".";
121
122    /** Keeps ClassDesc objects for stack of declared classes. */
123    private Deque<ClassDesc> classes;
124
125    /** Full qualified name of the package. */
126    private String packageName;
127
128    @Override
129    public int[] getDefaultTokens() {
130        return getRequiredTokens();
131    }
132
133    @Override
134    public int[] getAcceptableTokens() {
135        return getRequiredTokens();
136    }
137
138    @Override
139    public int[] getRequiredTokens() {
140        return new int[] {
141            TokenTypes.CLASS_DEF,
142            TokenTypes.CTOR_DEF,
143            TokenTypes.PACKAGE_DEF,
144            TokenTypes.LITERAL_NEW,
145        };
146    }
147
148    @Override
149    public void beginTree(DetailAST rootAST) {
150        classes = new ArrayDeque<>();
151        packageName = "";
152    }
153
154    @Override
155    public void visitToken(DetailAST ast) {
156        final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
157
158        switch (ast.getType()) {
159            case TokenTypes.PACKAGE_DEF:
160                packageName = extractQualifiedName(ast.getFirstChild().getNextSibling());
161                break;
162
163            case TokenTypes.CLASS_DEF:
164                registerNestedSubclassToOuterSuperClasses(ast);
165
166                final boolean isFinal = modifiers.findFirstToken(TokenTypes.FINAL) != null;
167                final boolean isAbstract = modifiers.findFirstToken(TokenTypes.ABSTRACT) != null;
168
169                final String qualifiedClassName = getQualifiedClassName(ast);
170                classes.push(new ClassDesc(qualifiedClassName, isFinal, isAbstract));
171                break;
172
173            case TokenTypes.CTOR_DEF:
174                if (!ScopeUtil.isInEnumBlock(ast) && !ScopeUtil.isInRecordBlock(ast)) {
175                    final ClassDesc desc = classes.peek();
176                    if (modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) {
177                        desc.registerNonPrivateCtor();
178                    }
179                    else {
180                        desc.registerPrivateCtor();
181                    }
182                }
183                break;
184
185            case TokenTypes.LITERAL_NEW:
186                if (ast.getFirstChild() != null
187                        && ast.getLastChild().getType() == TokenTypes.OBJBLOCK) {
188                    for (ClassDesc classDesc : classes) {
189                        if (doesNameOfClassMatchAnonymousInnerClassName(ast, classDesc)) {
190                            classDesc.registerAnonymousInnerClass();
191                        }
192                    }
193                }
194                break;
195
196            default:
197                throw new IllegalStateException(ast.toString());
198        }
199    }
200
201    @Override
202    public void leaveToken(DetailAST ast) {
203        if (ast.getType() == TokenTypes.CLASS_DEF) {
204            final ClassDesc desc = classes.pop();
205            if (desc.isWithPrivateCtor()
206                && !(desc.isDeclaredAsAbstract()
207                    || desc.isWithAnonymousInnerClass())
208                && !desc.isDeclaredAsFinal()
209                && !desc.isWithNonPrivateCtor()
210                && !desc.isWithNestedSubclass()
211                && !ScopeUtil.isInInterfaceOrAnnotationBlock(ast)) {
212                final String qualifiedName = desc.getQualifiedName();
213                final String className = getClassNameFromQualifiedName(qualifiedName);
214                log(ast, MSG_KEY, className);
215            }
216        }
217    }
218
219    /**
220     * Get name of class (with qualified package if specified) in {@code ast}.
221     *
222     * @param ast ast to extract class name from
223     * @return qualified name
224     */
225    private static String extractQualifiedName(DetailAST ast) {
226        return FullIdent.createFullIdent(ast).getText();
227    }
228
229    /**
230     * Register to outer super classes of given classAst that
231     * given classAst is extending them.
232     *
233     * @param classAst class which outer super classes will be
234     *                 informed about nesting subclass
235     */
236    private void registerNestedSubclassToOuterSuperClasses(DetailAST classAst) {
237        final String currentAstSuperClassName = getSuperClassName(classAst);
238        if (currentAstSuperClassName != null) {
239            for (ClassDesc classDesc : classes) {
240                final String classDescQualifiedName = classDesc.getQualifiedName();
241                if (doesNameInExtendMatchSuperClassName(classDescQualifiedName,
242                        currentAstSuperClassName)) {
243                    classDesc.registerNestedSubclass();
244                }
245            }
246        }
247    }
248
249    /**
250     * Check if class name matches with anonymous inner class name.
251     *
252     * @param ast current ast.
253     * @param classDesc class to match.
254     * @return true if current class name matches anonymous inner
255     *         class name.
256     */
257    private static boolean doesNameOfClassMatchAnonymousInnerClassName(DetailAST ast,
258                                                               ClassDesc classDesc) {
259        final String[] className = classDesc.getQualifiedName().split("\\.");
260        return ast.getFirstChild().getText().equals(className[className.length - 1]);
261    }
262
263    /**
264     * Get qualified class name from given class Ast.
265     *
266     * @param classAst class to get qualified class name
267     * @return qualified class name of a class
268     */
269    private String getQualifiedClassName(DetailAST classAst) {
270        final String className = classAst.findFirstToken(TokenTypes.IDENT).getText();
271        String outerClassQualifiedName = null;
272        if (!classes.isEmpty()) {
273            outerClassQualifiedName = classes.peek().getQualifiedName();
274        }
275        return getQualifiedClassName(packageName, outerClassQualifiedName, className);
276    }
277
278    /**
279     * Calculate qualified class name(package + class name) laying inside given
280     * outer class.
281     *
282     * @param packageName package name, empty string on default package
283     * @param outerClassQualifiedName qualified name(package + class) of outer class,
284     *                           null if doesn't exist
285     * @param className class name
286     * @return qualified class name(package + class name)
287     */
288    private static String getQualifiedClassName(String packageName, String outerClassQualifiedName,
289                                                String className) {
290        final String qualifiedClassName;
291
292        if (outerClassQualifiedName == null) {
293            if (packageName.isEmpty()) {
294                qualifiedClassName = className;
295            }
296            else {
297                qualifiedClassName = packageName + PACKAGE_SEPARATOR + className;
298            }
299        }
300        else {
301            qualifiedClassName = outerClassQualifiedName + PACKAGE_SEPARATOR + className;
302        }
303        return qualifiedClassName;
304    }
305
306    /**
307     * Get super class name of given class.
308     *
309     * @param classAst class
310     * @return super class name or null if super class is not specified
311     */
312    private static String getSuperClassName(DetailAST classAst) {
313        String superClassName = null;
314        final DetailAST classExtend = classAst.findFirstToken(TokenTypes.EXTENDS_CLAUSE);
315        if (classExtend != null) {
316            superClassName = extractQualifiedName(classExtend.getFirstChild());
317        }
318        return superClassName;
319    }
320
321    /**
322     * Checks if given super class name in extend clause match super class qualified name.
323     *
324     * @param superClassQualifiedName super class qualified name (with package)
325     * @param superClassInExtendClause name in extend clause
326     * @return true if given super class name in extend clause match super class qualified name,
327     *         false otherwise
328     */
329    private static boolean doesNameInExtendMatchSuperClassName(String superClassQualifiedName,
330                                                               String superClassInExtendClause) {
331        String superClassNormalizedName = superClassQualifiedName;
332        if (!superClassInExtendClause.contains(PACKAGE_SEPARATOR)) {
333            superClassNormalizedName = getClassNameFromQualifiedName(superClassQualifiedName);
334        }
335        return superClassNormalizedName.equals(superClassInExtendClause);
336    }
337
338    /**
339     * Get class name from qualified name.
340     *
341     * @param qualifiedName qualified class name
342     * @return class name
343     */
344    private static String getClassNameFromQualifiedName(String qualifiedName) {
345        return qualifiedName.substring(qualifiedName.lastIndexOf(PACKAGE_SEPARATOR) + 1);
346    }
347
348    /** Maintains information about class' ctors. */
349    private static final class ClassDesc {
350
351        /** Qualified class name(with package). */
352        private final String qualifiedName;
353
354        /** Is class declared as final. */
355        private final boolean declaredAsFinal;
356
357        /** Is class declared as abstract. */
358        private final boolean declaredAsAbstract;
359
360        /** Does class have non-private ctors. */
361        private boolean withNonPrivateCtor;
362
363        /** Does class have private ctors. */
364        private boolean withPrivateCtor;
365
366        /** Does class have nested subclass. */
367        private boolean withNestedSubclass;
368
369        /** Does class have anonymous inner class. */
370        private boolean withAnonymousInnerClass;
371
372        /**
373         *  Create a new ClassDesc instance.
374         *
375         *  @param qualifiedName qualified class name(with package)
376         *  @param declaredAsFinal indicates if the
377         *         class declared as final
378         *  @param declaredAsAbstract indicates if the
379         *         class declared as abstract
380         */
381        /* package */ ClassDesc(String qualifiedName, boolean declaredAsFinal,
382                boolean declaredAsAbstract) {
383            this.qualifiedName = qualifiedName;
384            this.declaredAsFinal = declaredAsFinal;
385            this.declaredAsAbstract = declaredAsAbstract;
386        }
387
388        /**
389         * Get qualified class name.
390         *
391         * @return qualified class name
392         */
393        private String getQualifiedName() {
394            return qualifiedName;
395        }
396
397        /** Adds private ctor. */
398        private void registerPrivateCtor() {
399            withPrivateCtor = true;
400        }
401
402        /** Adds non-private ctor. */
403        private void registerNonPrivateCtor() {
404            withNonPrivateCtor = true;
405        }
406
407        /** Adds nested subclass. */
408        private void registerNestedSubclass() {
409            withNestedSubclass = true;
410        }
411
412        /** Adds anonymous inner class. */
413        private void registerAnonymousInnerClass() {
414            withAnonymousInnerClass = true;
415        }
416
417        /**
418         *  Does class have private ctors.
419         *
420         *  @return true if class has private ctors
421         */
422        private boolean isWithPrivateCtor() {
423            return withPrivateCtor;
424        }
425
426        /**
427         *  Does class have non-private ctors.
428         *
429         *  @return true if class has non-private ctors
430         */
431        private boolean isWithNonPrivateCtor() {
432            return withNonPrivateCtor;
433        }
434
435        /**
436         * Does class have nested subclass.
437         *
438         * @return true if class has nested subclass
439         */
440        private boolean isWithNestedSubclass() {
441            return withNestedSubclass;
442        }
443
444        /**
445         *  Is class declared as final.
446         *
447         *  @return true if class is declared as final
448         */
449        private boolean isDeclaredAsFinal() {
450            return declaredAsFinal;
451        }
452
453        /**
454         *  Is class declared as abstract.
455         *
456         *  @return true if class is declared as final
457         */
458        private boolean isDeclaredAsAbstract() {
459            return declaredAsAbstract;
460        }
461
462        /**
463         * Does class have an anonymous inner class.
464         *
465         * @return true if class has anonymous inner class
466         */
467        private boolean isWithAnonymousInnerClass() {
468            return withAnonymousInnerClass;
469        }
470
471    }
472
473}