001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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;
024import java.util.LinkedList;
025import java.util.List;
026
027import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
028import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
029import com.puppycrawl.tools.checkstyle.api.DetailAST;
030import com.puppycrawl.tools.checkstyle.api.TokenTypes;
031import com.puppycrawl.tools.checkstyle.utils.ScopeUtils;
032
033/**
034 * <p>
035 * Checks that class which has only private ctors
036 * is declared as final. Doesn't check for classes nested in interfaces
037 * or annotations, as they are always {@code final} there.
038 * </p>
039 * <p>
040 * An example of how to configure the check is:
041 * </p>
042 * <pre>
043 * &lt;module name="FinalClass"/&gt;
044 * </pre>
045 * @author o_sukhodolsky
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
095            case TokenTypes.PACKAGE_DEF:
096                packageName = extractQualifiedName(ast);
097                break;
098
099            case TokenTypes.CLASS_DEF:
100                registerNestedSubclassToOuterSuperClasses(ast);
101
102                final boolean isFinal = modifiers.findFirstToken(TokenTypes.FINAL) != null;
103                final boolean isAbstract = modifiers.findFirstToken(TokenTypes.ABSTRACT) != null;
104
105                final String qualifiedClassName = getQualifiedClassName(ast);
106                classes.push(new ClassDesc(qualifiedClassName, isFinal, isAbstract));
107                break;
108
109            case TokenTypes.CTOR_DEF:
110                if (!ScopeUtils.isInEnumBlock(ast)) {
111                    final ClassDesc desc = classes.peek();
112                    if (modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) {
113                        desc.registerNonPrivateCtor();
114                    }
115                    else {
116                        desc.registerPrivateCtor();
117                    }
118                }
119                break;
120
121            default:
122                throw new IllegalStateException(ast.toString());
123        }
124    }
125
126    @Override
127    public void leaveToken(DetailAST ast) {
128        if (ast.getType() == TokenTypes.CLASS_DEF) {
129            final ClassDesc desc = classes.pop();
130            if (desc.isWithPrivateCtor()
131                && !desc.isDeclaredAsAbstract()
132                && !desc.isDeclaredAsFinal()
133                && !desc.isWithNonPrivateCtor()
134                && !desc.isWithNestedSubclass()
135                && !ScopeUtils.isInInterfaceOrAnnotationBlock(ast)) {
136                final String qualifiedName = desc.getQualifiedName();
137                final String className = getClassNameFromQualifiedName(qualifiedName);
138                log(ast.getLineNo(), MSG_KEY, className);
139            }
140        }
141    }
142
143    /**
144     * Get name of class(with qualified package if specified) in extend clause.
145     * @param classExtend extend clause to extract class name
146     * @return super class name
147     */
148    private static String extractQualifiedName(DetailAST classExtend) {
149        final String className;
150
151        if (classExtend.findFirstToken(TokenTypes.IDENT) == null) {
152            // Name specified with packages, have to traverse DOT
153            final DetailAST firstChild = classExtend.findFirstToken(TokenTypes.DOT);
154            final List<String> qualifiedNameParts = new LinkedList<>();
155
156            qualifiedNameParts.add(0, firstChild.findFirstToken(TokenTypes.IDENT).getText());
157            DetailAST traverse = firstChild.findFirstToken(TokenTypes.DOT);
158            while (traverse != null) {
159                qualifiedNameParts.add(0, traverse.findFirstToken(TokenTypes.IDENT).getText());
160                traverse = traverse.findFirstToken(TokenTypes.DOT);
161            }
162            className = String.join(PACKAGE_SEPARATOR, qualifiedNameParts);
163        }
164        else {
165            className = classExtend.findFirstToken(TokenTypes.IDENT).getText();
166        }
167
168        return className;
169    }
170
171    /**
172     * Register to outer super classes of given classAst that
173     * given classAst is extending them.
174     * @param classAst class which outer super classes will be
175     *                 informed about nesting subclass
176     */
177    private void registerNestedSubclassToOuterSuperClasses(DetailAST classAst) {
178        final String currentAstSuperClassName = getSuperClassName(classAst);
179        if (currentAstSuperClassName != null) {
180            for (ClassDesc classDesc : classes) {
181                final String classDescQualifiedName = classDesc.getQualifiedName();
182                if (doesNameInExtendMatchSuperClassName(classDescQualifiedName,
183                        currentAstSuperClassName)) {
184                    classDesc.registerNestedSubclass();
185                }
186            }
187        }
188    }
189
190    /**
191     * Get qualified class name from given class Ast.
192     * @param classAst class to get qualified class name
193     * @return qualified class name of a class
194     */
195    private String getQualifiedClassName(DetailAST classAst) {
196        final String className = classAst.findFirstToken(TokenTypes.IDENT).getText();
197        String outerClassQualifiedName = null;
198        if (!classes.isEmpty()) {
199            outerClassQualifiedName = classes.peek().getQualifiedName();
200        }
201        return getQualifiedClassName(packageName, outerClassQualifiedName, className);
202    }
203
204    /**
205     * Calculate qualified class name(package + class name) laying inside given
206     * outer class.
207     * @param packageName package name, empty string on default package
208     * @param outerClassQualifiedName qualified name(package + class) of outer class,
209     *                           null if doesn't exist
210     * @param className class name
211     * @return qualified class name(package + class name)
212     */
213    private static String getQualifiedClassName(String packageName, String outerClassQualifiedName,
214                                                String className) {
215        final String qualifiedClassName;
216
217        if (outerClassQualifiedName == null) {
218            if (packageName.isEmpty()) {
219                qualifiedClassName = className;
220            }
221            else {
222                qualifiedClassName = packageName + PACKAGE_SEPARATOR + className;
223            }
224        }
225        else {
226            qualifiedClassName = outerClassQualifiedName + PACKAGE_SEPARATOR + className;
227        }
228        return qualifiedClassName;
229    }
230
231    /**
232     * Get super class name of given class.
233     * @param classAst class
234     * @return super class name or null if super class is not specified
235     */
236    private static String getSuperClassName(DetailAST classAst) {
237        String superClassName = null;
238        final DetailAST classExtend = classAst.findFirstToken(TokenTypes.EXTENDS_CLAUSE);
239        if (classExtend != null) {
240            superClassName = extractQualifiedName(classExtend);
241        }
242        return superClassName;
243    }
244
245    /**
246     * Checks if given super class name in extend clause match super class qualified name.
247     * @param superClassQualifiedName super class qualified name (with package)
248     * @param superClassInExtendClause name in extend clause
249     * @return true if given super class name in extend clause match super class qualified name,
250     *         false otherwise
251     */
252    private static boolean doesNameInExtendMatchSuperClassName(String superClassQualifiedName,
253                                                               String superClassInExtendClause) {
254        String superClassNormalizedName = superClassQualifiedName;
255        if (!superClassInExtendClause.contains(PACKAGE_SEPARATOR)) {
256            superClassNormalizedName = getClassNameFromQualifiedName(superClassQualifiedName);
257        }
258        return superClassNormalizedName.equals(superClassInExtendClause);
259    }
260
261    /**
262     * Get class name from qualified name.
263     * @param qualifiedName qualified class name
264     * @return class name
265     */
266    private static String getClassNameFromQualifiedName(String qualifiedName) {
267        return qualifiedName.substring(qualifiedName.lastIndexOf(PACKAGE_SEPARATOR) + 1);
268    }
269
270    /** Maintains information about class' ctors. */
271    private static final class ClassDesc {
272        /** Qualified class name(with package). */
273        private final String qualifiedName;
274
275        /** Is class declared as final. */
276        private final boolean declaredAsFinal;
277
278        /** Is class declared as abstract. */
279        private final boolean declaredAsAbstract;
280
281        /** Does class have non-private ctors. */
282        private boolean withNonPrivateCtor;
283
284        /** Does class have private ctors. */
285        private boolean withPrivateCtor;
286
287        /** Does class have nested subclass. */
288        private boolean withNestedSubclass;
289
290        /**
291         *  Create a new ClassDesc instance.
292         *  @param qualifiedName qualified class name(with package)
293         *  @param declaredAsFinal indicates if the
294         *         class declared as final
295         *  @param declaredAsAbstract indicates if the
296         *         class declared as abstract
297         */
298        ClassDesc(String qualifiedName, boolean declaredAsFinal, boolean declaredAsAbstract) {
299            this.qualifiedName = qualifiedName;
300            this.declaredAsFinal = declaredAsFinal;
301            this.declaredAsAbstract = declaredAsAbstract;
302        }
303
304        /**
305         * Get qualified class name.
306         * @return qualified class name
307         */
308        private String getQualifiedName() {
309            return qualifiedName;
310        }
311
312        /** Adds private ctor. */
313        private void registerPrivateCtor() {
314            withPrivateCtor = true;
315        }
316
317        /** Adds non-private ctor. */
318        private void registerNonPrivateCtor() {
319            withNonPrivateCtor = true;
320        }
321
322        /** Adds nested subclass. */
323        private void registerNestedSubclass() {
324            withNestedSubclass = true;
325        }
326
327        /**
328         *  Does class have private ctors.
329         *  @return true if class has private ctors
330         */
331        private boolean isWithPrivateCtor() {
332            return withPrivateCtor;
333        }
334
335        /**
336         *  Does class have non-private ctors.
337         *  @return true if class has non-private ctors
338         */
339        private boolean isWithNonPrivateCtor() {
340            return withNonPrivateCtor;
341        }
342
343        /**
344         * Does class have nested subclass.
345         * @return true if class has nested subclass
346         */
347        private boolean isWithNestedSubclass() {
348            return withNestedSubclass;
349        }
350
351        /**
352         *  Is class declared as final.
353         *  @return true if class is declared as final
354         */
355        private boolean isDeclaredAsFinal() {
356            return declaredAsFinal;
357        }
358
359        /**
360         *  Is class declared as abstract.
361         *  @return true if class is declared as final
362         */
363        private boolean isDeclaredAsAbstract() {
364            return declaredAsAbstract;
365        }
366    }
367}