001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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 * </pre>
084 * <p>
085 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
086 * </p>
087 * <p>
088 * Violation Message Keys:
089 * </p>
090 * <ul>
091 * <li>
092 * {@code final.class}
093 * </li>
094 * </ul>
095 *
096 * @since 3.1
097 */
098@FileStatefulCheck
099public class FinalClassCheck
100    extends AbstractCheck {
101
102    /**
103     * A key is pointing to the warning message text in "messages.properties"
104     * file.
105     */
106    public static final String MSG_KEY = "final.class";
107
108    /**
109     * Character separate package names in qualified name of java class.
110     */
111    private static final String PACKAGE_SEPARATOR = ".";
112
113    /** Keeps ClassDesc objects for stack of declared classes. */
114    private Deque<ClassDesc> classes;
115
116    /** Full qualified name of the package. */
117    private String packageName;
118
119    @Override
120    public int[] getDefaultTokens() {
121        return getRequiredTokens();
122    }
123
124    @Override
125    public int[] getAcceptableTokens() {
126        return getRequiredTokens();
127    }
128
129    @Override
130    public int[] getRequiredTokens() {
131        return new int[] {TokenTypes.CLASS_DEF, TokenTypes.CTOR_DEF, TokenTypes.PACKAGE_DEF};
132    }
133
134    @Override
135    public void beginTree(DetailAST rootAST) {
136        classes = new ArrayDeque<>();
137        packageName = "";
138    }
139
140    @Override
141    public void visitToken(DetailAST ast) {
142        final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
143
144        switch (ast.getType()) {
145            case TokenTypes.PACKAGE_DEF:
146                packageName = extractQualifiedName(ast.getFirstChild().getNextSibling());
147                break;
148
149            case TokenTypes.CLASS_DEF:
150                registerNestedSubclassToOuterSuperClasses(ast);
151
152                final boolean isFinal = modifiers.findFirstToken(TokenTypes.FINAL) != null;
153                final boolean isAbstract = modifiers.findFirstToken(TokenTypes.ABSTRACT) != null;
154
155                final String qualifiedClassName = getQualifiedClassName(ast);
156                classes.push(new ClassDesc(qualifiedClassName, isFinal, isAbstract));
157                break;
158
159            case TokenTypes.CTOR_DEF:
160                if (!ScopeUtil.isInEnumBlock(ast) && !ScopeUtil.isInRecordBlock(ast)) {
161                    final ClassDesc desc = classes.peek();
162                    if (modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) {
163                        desc.registerNonPrivateCtor();
164                    }
165                    else {
166                        desc.registerPrivateCtor();
167                    }
168                }
169                break;
170
171            default:
172                throw new IllegalStateException(ast.toString());
173        }
174    }
175
176    @Override
177    public void leaveToken(DetailAST ast) {
178        if (ast.getType() == TokenTypes.CLASS_DEF) {
179            final ClassDesc desc = classes.pop();
180            if (desc.isWithPrivateCtor()
181                && !desc.isDeclaredAsAbstract()
182                && !desc.isDeclaredAsFinal()
183                && !desc.isWithNonPrivateCtor()
184                && !desc.isWithNestedSubclass()
185                && !ScopeUtil.isInInterfaceOrAnnotationBlock(ast)) {
186                final String qualifiedName = desc.getQualifiedName();
187                final String className = getClassNameFromQualifiedName(qualifiedName);
188                log(ast, MSG_KEY, className);
189            }
190        }
191    }
192
193    /**
194     * Get name of class (with qualified package if specified) in {@code ast}.
195     *
196     * @param ast ast to extract class name from
197     * @return qualified name
198     */
199    private static String extractQualifiedName(DetailAST ast) {
200        return FullIdent.createFullIdent(ast).getText();
201    }
202
203    /**
204     * Register to outer super classes of given classAst that
205     * given classAst is extending them.
206     *
207     * @param classAst class which outer super classes will be
208     *                 informed about nesting subclass
209     */
210    private void registerNestedSubclassToOuterSuperClasses(DetailAST classAst) {
211        final String currentAstSuperClassName = getSuperClassName(classAst);
212        if (currentAstSuperClassName != null) {
213            for (ClassDesc classDesc : classes) {
214                final String classDescQualifiedName = classDesc.getQualifiedName();
215                if (doesNameInExtendMatchSuperClassName(classDescQualifiedName,
216                        currentAstSuperClassName)) {
217                    classDesc.registerNestedSubclass();
218                }
219            }
220        }
221    }
222
223    /**
224     * Get qualified class name from given class Ast.
225     *
226     * @param classAst class to get qualified class name
227     * @return qualified class name of a class
228     */
229    private String getQualifiedClassName(DetailAST classAst) {
230        final String className = classAst.findFirstToken(TokenTypes.IDENT).getText();
231        String outerClassQualifiedName = null;
232        if (!classes.isEmpty()) {
233            outerClassQualifiedName = classes.peek().getQualifiedName();
234        }
235        return getQualifiedClassName(packageName, outerClassQualifiedName, className);
236    }
237
238    /**
239     * Calculate qualified class name(package + class name) laying inside given
240     * outer class.
241     *
242     * @param packageName package name, empty string on default package
243     * @param outerClassQualifiedName qualified name(package + class) of outer class,
244     *                           null if doesn't exist
245     * @param className class name
246     * @return qualified class name(package + class name)
247     */
248    private static String getQualifiedClassName(String packageName, String outerClassQualifiedName,
249                                                String className) {
250        final String qualifiedClassName;
251
252        if (outerClassQualifiedName == null) {
253            if (packageName.isEmpty()) {
254                qualifiedClassName = className;
255            }
256            else {
257                qualifiedClassName = packageName + PACKAGE_SEPARATOR + className;
258            }
259        }
260        else {
261            qualifiedClassName = outerClassQualifiedName + PACKAGE_SEPARATOR + className;
262        }
263        return qualifiedClassName;
264    }
265
266    /**
267     * Get super class name of given class.
268     *
269     * @param classAst class
270     * @return super class name or null if super class is not specified
271     */
272    private static String getSuperClassName(DetailAST classAst) {
273        String superClassName = null;
274        final DetailAST classExtend = classAst.findFirstToken(TokenTypes.EXTENDS_CLAUSE);
275        if (classExtend != null) {
276            superClassName = extractQualifiedName(classExtend.getFirstChild());
277        }
278        return superClassName;
279    }
280
281    /**
282     * Checks if given super class name in extend clause match super class qualified name.
283     *
284     * @param superClassQualifiedName super class qualified name (with package)
285     * @param superClassInExtendClause name in extend clause
286     * @return true if given super class name in extend clause match super class qualified name,
287     *         false otherwise
288     */
289    private static boolean doesNameInExtendMatchSuperClassName(String superClassQualifiedName,
290                                                               String superClassInExtendClause) {
291        String superClassNormalizedName = superClassQualifiedName;
292        if (!superClassInExtendClause.contains(PACKAGE_SEPARATOR)) {
293            superClassNormalizedName = getClassNameFromQualifiedName(superClassQualifiedName);
294        }
295        return superClassNormalizedName.equals(superClassInExtendClause);
296    }
297
298    /**
299     * Get class name from qualified name.
300     *
301     * @param qualifiedName qualified class name
302     * @return class name
303     */
304    private static String getClassNameFromQualifiedName(String qualifiedName) {
305        return qualifiedName.substring(qualifiedName.lastIndexOf(PACKAGE_SEPARATOR) + 1);
306    }
307
308    /** Maintains information about class' ctors. */
309    private static final class ClassDesc {
310
311        /** Qualified class name(with package). */
312        private final String qualifiedName;
313
314        /** Is class declared as final. */
315        private final boolean declaredAsFinal;
316
317        /** Is class declared as abstract. */
318        private final boolean declaredAsAbstract;
319
320        /** Does class have non-private ctors. */
321        private boolean withNonPrivateCtor;
322
323        /** Does class have private ctors. */
324        private boolean withPrivateCtor;
325
326        /** Does class have nested subclass. */
327        private boolean withNestedSubclass;
328
329        /**
330         *  Create a new ClassDesc instance.
331         *
332         *  @param qualifiedName qualified class name(with package)
333         *  @param declaredAsFinal indicates if the
334         *         class declared as final
335         *  @param declaredAsAbstract indicates if the
336         *         class declared as abstract
337         */
338        /* package */ ClassDesc(String qualifiedName, boolean declaredAsFinal,
339                boolean declaredAsAbstract) {
340            this.qualifiedName = qualifiedName;
341            this.declaredAsFinal = declaredAsFinal;
342            this.declaredAsAbstract = declaredAsAbstract;
343        }
344
345        /**
346         * Get qualified class name.
347         *
348         * @return qualified class name
349         */
350        private String getQualifiedName() {
351            return qualifiedName;
352        }
353
354        /** Adds private ctor. */
355        private void registerPrivateCtor() {
356            withPrivateCtor = true;
357        }
358
359        /** Adds non-private ctor. */
360        private void registerNonPrivateCtor() {
361            withNonPrivateCtor = true;
362        }
363
364        /** Adds nested subclass. */
365        private void registerNestedSubclass() {
366            withNestedSubclass = true;
367        }
368
369        /**
370         *  Does class have private ctors.
371         *
372         *  @return true if class has private ctors
373         */
374        private boolean isWithPrivateCtor() {
375            return withPrivateCtor;
376        }
377
378        /**
379         *  Does class have non-private ctors.
380         *
381         *  @return true if class has non-private ctors
382         */
383        private boolean isWithNonPrivateCtor() {
384            return withNonPrivateCtor;
385        }
386
387        /**
388         * Does class have nested subclass.
389         *
390         * @return true if class has nested subclass
391         */
392        private boolean isWithNestedSubclass() {
393            return withNestedSubclass;
394        }
395
396        /**
397         *  Is class declared as final.
398         *
399         *  @return true if class is declared as final
400         */
401        private boolean isDeclaredAsFinal() {
402            return declaredAsFinal;
403        }
404
405        /**
406         *  Is class declared as abstract.
407         *
408         *  @return true if class is declared as final
409         */
410        private boolean isDeclaredAsAbstract() {
411            return declaredAsAbstract;
412        }
413
414    }
415
416}