//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2017 the original author or authors. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //////////////////////////////////////////////////////////////////////////////// package com.puppycrawl.tools.checkstyle.checks.design; import java.util.ArrayDeque; import java.util.Deque; import java.util.LinkedList; import java.util.List; import com.puppycrawl.tools.checkstyle.api.AbstractCheck; import com.puppycrawl.tools.checkstyle.api.DetailAST; import com.puppycrawl.tools.checkstyle.api.TokenTypes; import com.puppycrawl.tools.checkstyle.utils.ScopeUtils; /** * <p> * Checks that class which has only private ctors * is declared as final. Doesn't check for classes nested in interfaces * or annotations, as they are always <code>final</code> there. * </p> * <p> * An example of how to configure the check is: * </p> * <pre> * <module name="FinalClass"/> * </pre> * @author o_sukhodolsky */ public class FinalClassCheck extends AbstractCheck { /** * A key is pointing to the warning message text in "messages.properties" * file. */ public static final String MSG_KEY = "final.class"; /** * Character separate package names in qualified name of java class. */ public static final String PACKAGE_SEPARATOR = "."; /** Keeps ClassDesc objects for stack of declared classes. */ private Deque<ClassDesc> classes; /** Full qualified name of the package. */ private String packageName; @Override public int[] getDefaultTokens() { return getAcceptableTokens(); } @Override public int[] getAcceptableTokens() { return new int[] {TokenTypes.CLASS_DEF, TokenTypes.CTOR_DEF, TokenTypes.PACKAGE_DEF}; } @Override public int[] getRequiredTokens() { return getAcceptableTokens(); } @Override public void beginTree(DetailAST rootAST) { classes = new ArrayDeque<>(); packageName = ""; } @Override public void visitToken(DetailAST ast) { final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS); switch (ast.getType()) { case TokenTypes.PACKAGE_DEF: packageName = extractQualifiedName(ast); break; case TokenTypes.CLASS_DEF: registerNestedSubclassToOuterSuperClasses(ast); final boolean isFinal = modifiers.branchContains(TokenTypes.FINAL); final boolean isAbstract = modifiers.branchContains(TokenTypes.ABSTRACT); final String qualifiedClassName = getQualifiedClassName(ast); classes.push(new ClassDesc(qualifiedClassName, isFinal, isAbstract)); break; case TokenTypes.CTOR_DEF: if (!ScopeUtils.isInEnumBlock(ast)) { final ClassDesc desc = classes.peek(); if (modifiers.branchContains(TokenTypes.LITERAL_PRIVATE)) { desc.registerPrivateCtor(); } else { desc.registerNonPrivateCtor(); } } break; default: throw new IllegalStateException(ast.toString()); } } @Override public void leaveToken(DetailAST ast) { if (ast.getType() == TokenTypes.CLASS_DEF) { final ClassDesc desc = classes.pop(); if (desc.isWithPrivateCtor() && !desc.isDeclaredAsAbstract() && !desc.isDeclaredAsFinal() && !desc.isWithNonPrivateCtor() && !desc.isWithNestedSubclass() && !ScopeUtils.isInInterfaceOrAnnotationBlock(ast)) { final String qualifiedName = desc.getQualifiedName(); final String className = getClassNameFromQualifiedName(qualifiedName); log(ast.getLineNo(), MSG_KEY, className); } } } /** * Get name of class(with qualified package if specified) in extend clause. * @param classExtend extend clause to extract class name * @return super class name */ private static String extractQualifiedName(DetailAST classExtend) { final String className; if (classExtend.findFirstToken(TokenTypes.IDENT) == null) { // Name specified with packages, have to traverse DOT final DetailAST firstChild = classExtend.findFirstToken(TokenTypes.DOT); final List<String> qualifiedNameParts = new LinkedList<>(); qualifiedNameParts.add(0, firstChild.findFirstToken(TokenTypes.IDENT).getText()); DetailAST traverse = firstChild.findFirstToken(TokenTypes.DOT); while (traverse != null) { qualifiedNameParts.add(0, traverse.findFirstToken(TokenTypes.IDENT).getText()); traverse = traverse.findFirstToken(TokenTypes.DOT); } className = String.join(PACKAGE_SEPARATOR, qualifiedNameParts); } else { className = classExtend.findFirstToken(TokenTypes.IDENT).getText(); } return className; } /** * Register to outer super classes of given classAst that * given classAst is extending them. * @param classAst class which outer super classes will be * informed about nesting subclass */ private void registerNestedSubclassToOuterSuperClasses(DetailAST classAst) { final String currentAstSuperClassName = getSuperClassName(classAst); if (currentAstSuperClassName != null) { for (ClassDesc classDesc : classes) { final String classDescQualifiedName = classDesc.getQualifiedName(); if (doesNameInExtendMatchSuperClassName(classDescQualifiedName, currentAstSuperClassName)) { classDesc.registerNestedSubclass(); } } } } /** * Get qualified class name from given class Ast. * @param classAst class to get qualified class name * @return qualified class name of a class */ private String getQualifiedClassName(DetailAST classAst) { final String className = classAst.findFirstToken(TokenTypes.IDENT).getText(); String outerClassQualifiedName = null; if (!classes.isEmpty()) { outerClassQualifiedName = classes.peek().getQualifiedName(); } return getQualifiedClassName(packageName, outerClassQualifiedName, className); } /** * Calculate qualified class name(package + class name) laying inside given * outer class. * @param packageName package name, empty string on default package * @param outerClassQualifiedName qualified name(package + class) of outer class, * null if doesn't exist * @param className class name * @return qualified class name(package + class name) */ private static String getQualifiedClassName(String packageName, String outerClassQualifiedName, String className) { final String qualifiedClassName; if (outerClassQualifiedName == null) { if (packageName.isEmpty()) { qualifiedClassName = className; } else { qualifiedClassName = packageName + PACKAGE_SEPARATOR + className; } } else { qualifiedClassName = outerClassQualifiedName + PACKAGE_SEPARATOR + className; } return qualifiedClassName; } /** * Get super class name of given class. * @param classAst class * @return super class name or null if super class is not specified */ private static String getSuperClassName(DetailAST classAst) { String superClassName = null; final DetailAST classExtend = classAst.findFirstToken(TokenTypes.EXTENDS_CLAUSE); if (classExtend != null) { superClassName = extractQualifiedName(classExtend); } return superClassName; } /** * Checks if given super class name in extend clause match super class qualified name. * @param superClassQualifiedName super class qualified name (with package) * @param superClassInExtendClause name in extend clause * @return true if given super class name in extend clause match super class qualified name, * false otherwise */ private static boolean doesNameInExtendMatchSuperClassName(String superClassQualifiedName, String superClassInExtendClause) { String superClassNormalizedName = superClassQualifiedName; if (!superClassInExtendClause.contains(PACKAGE_SEPARATOR)) { superClassNormalizedName = getClassNameFromQualifiedName(superClassQualifiedName); } return superClassNormalizedName.equals(superClassInExtendClause); } /** * Get class name from qualified name. * @param qualifiedName qualified class name * @return class name */ private static String getClassNameFromQualifiedName(String qualifiedName) { return qualifiedName.substring(qualifiedName.lastIndexOf(PACKAGE_SEPARATOR) + 1); } /** Maintains information about class' ctors. */ private static final class ClassDesc { /** Qualified class name(with package). */ private final String qualifiedName; /** Is class declared as final. */ private final boolean declaredAsFinal; /** Is class declared as abstract. */ private final boolean declaredAsAbstract; /** Does class have non-private ctors. */ private boolean withNonPrivateCtor; /** Does class have private ctors. */ private boolean withPrivateCtor; /** Does class have nested subclass. */ private boolean withNestedSubclass; /** * Create a new ClassDesc instance. * @param qualifiedName qualified class name(with package) * @param declaredAsFinal indicates if the * class declared as final * @param declaredAsAbstract indicates if the * class declared as abstract */ ClassDesc(String qualifiedName, boolean declaredAsFinal, boolean declaredAsAbstract) { this.qualifiedName = qualifiedName; this.declaredAsFinal = declaredAsFinal; this.declaredAsAbstract = declaredAsAbstract; } /** * Get qualified class name. * @return qualified class name */ private String getQualifiedName() { return qualifiedName; } /** Adds private ctor. */ private void registerPrivateCtor() { withPrivateCtor = true; } /** Adds non-private ctor. */ private void registerNonPrivateCtor() { withNonPrivateCtor = true; } /** Adds nested subclass. */ private void registerNestedSubclass() { withNestedSubclass = true; } /** * Does class have private ctors. * @return true if class has private ctors */ private boolean isWithPrivateCtor() { return withPrivateCtor; } /** * Does class have non-private ctors. * @return true if class has non-private ctors */ private boolean isWithNonPrivateCtor() { return withNonPrivateCtor; } /** * Does class have nested subclass. * @return true if class has nested subclass */ private boolean isWithNestedSubclass() { return withNestedSubclass; } /** * Is class declared as final. * @return true if class is declared as final */ private boolean isDeclaredAsFinal() { return declaredAsFinal; } /** * Is class declared as abstract. * @return true if class is declared as final */ private boolean isDeclaredAsAbstract() { return declaredAsAbstract; } } }