ProhibitNonFinalClassesCheck.java
/*
* Copyright (c) 2011-2024 Qulice.com
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met: 1) Redistributions of source code must retain the above
* copyright notice, this list of conditions and the following
* disclaimer. 2) Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution. 3) Neither the name of the Qulice.com nor
* the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
* NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.qulice.checkstyle;
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.ScopeUtil;
import java.util.ArrayDeque;
import java.util.Deque;
import org.cactoos.text.Joined;
import org.cactoos.text.UncheckedText;
/**
* Checks that classes are declared as final. Doesn't check for classes nested
* in interfaces or annotations, as they are always {@code final} there.
* <p>
* An example of how to configure the check is:
* </p>
* <pre>
* <module name="ProhibitNonFinalClassesCheck"/>
* </pre>
*
* @since 0.19
*/
public final class ProhibitNonFinalClassesCheck extends AbstractCheck {
/**
* Character separate package names in qualified name of java class.
*/
private static final String PACKAGE_SEPARATOR = ".";
/**
* Keeps ClassDesc objects for stack of declared classes.
*/
private Deque<ClassDesc> classes = new ArrayDeque<>();
/**
* Full qualified name of the package.
*/
private String pack;
@Override
public int[] getDefaultTokens() {
return this.getRequiredTokens();
}
@Override
public int[] getAcceptableTokens() {
return this.getRequiredTokens();
}
@Override
public int[] getRequiredTokens() {
return new int[] {TokenTypes.CLASS_DEF};
}
@Override
public void beginTree(final DetailAST root) {
this.classes = new ArrayDeque<>();
this.pack = "";
}
@Override
public void visitToken(final DetailAST ast) {
final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
if (ast.getType() == TokenTypes.CLASS_DEF) {
final boolean isfinal =
modifiers.findFirstToken(TokenTypes.FINAL) != null;
final boolean isabstract =
modifiers.findFirstToken(TokenTypes.ABSTRACT) != null;
final String qualified = this.getQualifiedClassName(ast);
this.classes.push(
new ClassDesc(qualified, isfinal, isabstract)
);
}
}
@Override
public void leaveToken(final DetailAST ast) {
if (ast.getType() == TokenTypes.CLASS_DEF) {
final ClassDesc desc = this.classes.pop();
if (!desc.isDeclaredAsAbstract()
&& !desc.isAsfinal()
&& !ScopeUtil.isInInterfaceOrAnnotationBlock(ast)) {
final String qualified = desc.getQualified();
final String name =
ProhibitNonFinalClassesCheck.getClassNameFromQualifiedName(
qualified
);
log(ast.getLineNo(), "Classes should be final", name);
}
}
}
/**
* 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(final DetailAST classast) {
final String name = classast.findFirstToken(
TokenTypes.IDENT
).getText();
String outer = null;
if (!this.classes.isEmpty()) {
outer = this.classes.peek().getQualified();
}
return ProhibitNonFinalClassesCheck.getQualifiedClassName(
this.pack,
outer,
name
);
}
/**
* Calculate qualified class name(package + class name) laying inside given
* outer class.
* @param pack Package name, empty string on default package
* @param outer Qualified name(package + class) of outer
* class, null if doesn't exist
* @param name Class name
* @return Qualified class name(package + class name)
*/
private static String getQualifiedClassName(
final String pack,
final String outer,
final String name) {
final String qualified;
if (outer == null) {
if (pack.isEmpty()) {
qualified = name;
} else {
qualified =
new UncheckedText(
new Joined(
ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR,
pack,
name
)
).asString();
}
} else {
qualified =
new UncheckedText(
new Joined(
ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR,
outer,
name
)
).asString();
}
return qualified;
}
/**
* Get class name from qualified name.
* @param qualified Qualified class name
* @return Class Name
*/
private static String getClassNameFromQualifiedName(
final String qualified
) {
return qualified.substring(
qualified.lastIndexOf(
ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR
) + 1
);
}
/**
* Maintains information about class' ctors.
*
* @since 0.1
*/
private static final class ClassDesc {
/**
* Qualified class name(with package).
*/
private final String qualified;
/**
* Is class declared as final.
*/
private final boolean asfinal;
/**
* Is class declared as abstract.
*/
private final boolean asabstract;
/**
* Create a new ClassDesc instance.
*
* @param qualified Qualified class name(with package)
* @param asfinal Indicates if the class declared as final
* @param asabstract Indicates if the class declared as
* abstract
*/
ClassDesc(final String qualified, final boolean asfinal,
final boolean asabstract
) {
this.qualified = qualified;
this.asfinal = asfinal;
this.asabstract = asabstract;
}
/**
* Get qualified class name.
* @return Qualified class name
*/
private String getQualified() {
return this.qualified;
}
/**
* Is class declared as final.
* @return True if class is declared as final
*/
private boolean isAsfinal() {
return this.asfinal;
}
/**
* Is class declared as abstract.
* @return True if class is declared as final
*/
private boolean isDeclaredAsAbstract() {
return this.asabstract;
}
}
}