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