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