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