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