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