Skip to content

Commit

Permalink
Issue checkstyle#10859: FinalClass now exempts private-only construct…
Browse files Browse the repository at this point in the history
…or classes that are extended by another class in the same compilation unit
  • Loading branch information
Vyom-Yadav committed Oct 26, 2021
1 parent 4b4df98 commit ed983f4
Show file tree
Hide file tree
Showing 4 changed files with 403 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.stream.Collectors;

import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
Expand Down Expand Up @@ -122,6 +124,12 @@ public class FinalClassCheck
/** Keeps ClassDesc objects for stack of declared classes. */
private Deque<ClassDesc> classes;

/**
* Keeps ClassDesc objects for stack of declared classes,
* pops the class out when all nested, inner, anonymous classes have been examined.
*/
private Deque<ClassDesc> classesPoppedOutAfterExamination;

/** Full qualified name of the package. */
private String packageName;

Expand All @@ -147,6 +155,7 @@ public int[] getRequiredTokens() {

@Override
public void beginTree(DetailAST rootAST) {
classesPoppedOutAfterExamination = new ArrayDeque<>();
classes = new ArrayDeque<>();
packageName = "";
}
Expand All @@ -161,18 +170,19 @@ public void visitToken(DetailAST ast) {
break;

case TokenTypes.CLASS_DEF:
registerNestedSubclassToOuterSuperClasses(ast);

final boolean isFinal = modifiers.findFirstToken(TokenTypes.FINAL) != null;
final boolean isAbstract = modifiers.findFirstToken(TokenTypes.ABSTRACT) != null;

final String qualifiedClassName = getQualifiedClassName(ast);
classes.push(new ClassDesc(qualifiedClassName, isFinal, isAbstract));
final ClassDesc currClass = new ClassDesc(
qualifiedClassName, isFinal, isAbstract, ast);
classesPoppedOutAfterExamination.push(currClass);
classes.push(currClass);
break;

case TokenTypes.CTOR_DEF:
if (!ScopeUtil.isInEnumBlock(ast) && !ScopeUtil.isInRecordBlock(ast)) {
final ClassDesc desc = classes.peek();
final ClassDesc desc = classesPoppedOutAfterExamination.peek();
if (modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) {
desc.registerNonPrivateCtor();
}
Expand All @@ -185,14 +195,13 @@ public void visitToken(DetailAST ast) {
case TokenTypes.LITERAL_NEW:
if (ast.getFirstChild() != null
&& ast.getLastChild().getType() == TokenTypes.OBJBLOCK) {
for (ClassDesc classDesc : classes) {
for (ClassDesc classDesc : classesPoppedOutAfterExamination) {
if (doesNameOfClassMatchAnonymousInnerClassName(ast, classDesc)) {
classDesc.registerAnonymousInnerClass();
}
}
}
break;

default:
throw new IllegalStateException(ast.toString());
}
Expand All @@ -201,19 +210,57 @@ public void visitToken(DetailAST ast) {
@Override
public void leaveToken(DetailAST ast) {
if (ast.getType() == TokenTypes.CLASS_DEF) {
final ClassDesc desc = classes.pop();
if (desc.isWithPrivateCtor()
classesPoppedOutAfterExamination.pop();
if (isTopLevelClass(ast)) {
classes.forEach(desc -> {
final DetailAST classAst = desc.getClassAst();
registerNestedSubclassToOuterSuperClasses(classAst, desc.getQualifiedName());
});
int size = classes.size();
while (size > 0) {
final ClassDesc pop = classes.pop();
final DetailAST classAst = pop.getClassAst();
if (shouldBeDeclaredAsFinal(pop, classAst)) {
final String qualifiedName = pop.getQualifiedName();
final String className = getClassNameFromQualifiedName(qualifiedName);
log(classAst, MSG_KEY, className);
}
size--;
}
}
}
}

/**
* Checks if the current class is one of the outermost class.
*
* @param classAst classAst
* @return true if current class is one of the outermost class
*/
private static boolean isTopLevelClass(DetailAST classAst) {
boolean result = true;
final DetailAST grandParent = classAst.getParent().getParent();
if (grandParent != null) {
result = grandParent.getType() != TokenTypes.CLASS_DEF;
}
return result;
}

/**
* Checks whether a class should be declared as final or not.
*
* @param desc description of the class
* @param classAst classAst
* @return true if given class should be declared as final otherwise false
*/
private static boolean shouldBeDeclaredAsFinal(ClassDesc desc, DetailAST classAst) {
return desc.isWithPrivateCtor()
&& !(desc.isDeclaredAsAbstract()
|| desc.isWithAnonymousInnerClass())
|| desc.isWithAnonymousInnerClass())
&& !desc.isDeclaredAsFinal()
&& !desc.isWithNonPrivateCtor()
&& !desc.isWithNestedSubclass()
&& !ScopeUtil.isInInterfaceOrAnnotationBlock(ast)) {
final String qualifiedName = desc.getQualifiedName();
final String className = getClassNameFromQualifiedName(qualifiedName);
log(ast, MSG_KEY, className);
}
}
&& !ScopeUtil.isInInterfaceOrAnnotationBlock(classAst);
}

/**
Expand All @@ -227,25 +274,120 @@ private static String extractQualifiedName(DetailAST ast) {
}

/**
* Register to outer super classes of given classAst that
* Register to outer super class of given classAst that
* given classAst is extending them.
*
* @param classAst class which outer super classes will be
* @param classAst class which outer super class will be
* informed about nesting subclass
* @param qualifiedClassName qualifies class name(with package) of the current class
*/
private void registerNestedSubclassToOuterSuperClasses(DetailAST classAst) {
private void registerNestedSubclassToOuterSuperClasses(DetailAST classAst,
String qualifiedClassName) {
final String currentAstSuperClassName = getSuperClassName(classAst);

if (currentAstSuperClassName != null) {
String classToBeRegisteredAsSuperClass = currentAstSuperClassName;

final List<ClassDesc> sameNamedClassList = classesWithSameName(
currentAstSuperClassName);
if (!sameNamedClassList.isEmpty()) {
classToBeRegisteredAsSuperClass = getClassToBeRegisteredAsSuperClass(
qualifiedClassName, sameNamedClassList);
}

for (ClassDesc classDesc : classes) {
final String classDescQualifiedName = classDesc.getQualifiedName();
if (doesNameInExtendMatchSuperClassName(classDescQualifiedName,
currentAstSuperClassName)) {
if (classToBeRegisteredAsSuperClass.equals(classDescQualifiedName)) {
classDesc.registerNestedSubclass();
break;
}
}
}
}

/**
* Checks if there is a class with same name.
*
* @param className name of the class
* @return true if there is another class with same name.
*/
private List<ClassDesc> classesWithSameName(String className) {
return classes.stream()
.filter(classDesc -> {
return classDesc.getQualifiedName().endsWith(
PACKAGE_SEPARATOR + className);
})
.collect(Collectors.toList());
}

/**
* Get name of the class to be registered as super class.
*
* @param currentClassName current class name
* @param classesWithSameName classes which same name as super class
* @return name of the class to be registered as super class
*/
private static String getClassToBeRegisteredAsSuperClass(String currentClassName,
List<ClassDesc> classesWithSameName) {
final char[] currentClassArray = currentClassName.toCharArray();
final int currentClassArrayLength = currentClassArray.length;
for (ClassDesc classes : classesWithSameName) {
final String classWithSameName = classes.getQualifiedName();
final char[] classWithSameNameArray = classWithSameName.toCharArray();
final int minLength = Math.min(currentClassArrayLength, classWithSameNameArray.length);
int counter = 0;
int countToBeRegistered = 0;
for (int i = 0; i < minLength; i++) {
if (currentClassArray[i] == classWithSameNameArray[i]) {
counter++;
if (currentClassArray[i] == PACKAGE_SEPARATOR.toCharArray()[0]) {
countToBeRegistered = counter;
}
}
else {
break;
}
}
classes.setClassNameMatchingCount(countToBeRegistered);
}
ClassDesc classToBeRegisteredAsSuperClass = classesWithSameName.get(0);
for (int i = 1; i < classesWithSameName.size(); i++) {
final ClassDesc classWithSameName = classesWithSameName.get(i);
if (classToBeRegisteredAsSuperClass.getClassNameMatchingCount()
< classWithSameName.getClassNameMatchingCount()) {
classToBeRegisteredAsSuperClass = classWithSameName;
}
else if (classToBeRegisteredAsSuperClass.getClassNameMatchingCount()
== classWithSameName.getClassNameMatchingCount()
&& isFormerClassNestedMoreThanTheLatterClass(
classToBeRegisteredAsSuperClass.getQualifiedName(),
classWithSameName.getQualifiedName())) {
classToBeRegisteredAsSuperClass = classWithSameName;
}
}

return classToBeRegisteredAsSuperClass.getQualifiedName();
}

/**
* Checks if former class is nested more than the latter class.
*
* @param formerClass former class
* @param latterClass latter class
* @return true if former class is nested more than the latter class
*/
private static boolean isFormerClassNestedMoreThanTheLatterClass(
String formerClass, String latterClass) {
final char packageSeparator = PACKAGE_SEPARATOR.toCharArray()[0];

return formerClass.chars()
.filter(character -> character == packageSeparator)
.count()
> latterClass.chars()
.filter(character -> character == packageSeparator)
.count();
}

/**
* Check if class name matches with anonymous inner class name.
*
Expand All @@ -269,8 +411,8 @@ private static boolean doesNameOfClassMatchAnonymousInnerClassName(DetailAST ast
private String getQualifiedClassName(DetailAST classAst) {
final String className = classAst.findFirstToken(TokenTypes.IDENT).getText();
String outerClassQualifiedName = null;
if (!classes.isEmpty()) {
outerClassQualifiedName = classes.peek().getQualifiedName();
if (!classesPoppedOutAfterExamination.isEmpty()) {
outerClassQualifiedName = classesPoppedOutAfterExamination.peek().getQualifiedName();
}
return getQualifiedClassName(packageName, outerClassQualifiedName, className);
}
Expand Down Expand Up @@ -318,23 +460,6 @@ private static String getSuperClassName(DetailAST classAst) {
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.
*
Expand All @@ -348,6 +473,9 @@ private static String getClassNameFromQualifiedName(String qualifiedName) {
/** Maintains information about class' ctors. */
private static final class ClassDesc {

/** Corresponding node. */
private final DetailAST classAst;

/** Qualified class name(with package). */
private final String qualifiedName;

Expand All @@ -369,6 +497,13 @@ private static final class ClassDesc {
/** Does class have anonymous inner class. */
private boolean withAnonymousInnerClass;

/**
* Counts how much the class's qualified name
* is similar to a given class. Only required when
* there are multiple classes with same name.
*/
private int classNameMatchingCount;

/**
* Create a new ClassDesc instance.
*
Expand All @@ -377,12 +512,14 @@ private static final class ClassDesc {
* class declared as final
* @param declaredAsAbstract indicates if the
* class declared as abstract
* @param classAst classAst node
*/
/* package */ ClassDesc(String qualifiedName, boolean declaredAsFinal,
boolean declaredAsAbstract) {
boolean declaredAsAbstract, DetailAST classAst) {
this.qualifiedName = qualifiedName;
this.declaredAsFinal = declaredAsFinal;
this.declaredAsAbstract = declaredAsAbstract;
this.classAst = classAst;
}

/**
Expand All @@ -394,6 +531,15 @@ private String getQualifiedName() {
return qualifiedName;
}

/**
* Get the classAst node.
*
* @return classAst node
*/
public DetailAST getClassAst() {
return classAst;
}

/** Adds private ctor. */
private void registerPrivateCtor() {
withPrivateCtor = true;
Expand Down Expand Up @@ -468,6 +614,22 @@ private boolean isWithAnonymousInnerClass() {
return withAnonymousInnerClass;
}

}
/**
* Get the classNameMatchingCount.
*
* @return classNameMatchingCount
*/
public int getClassNameMatchingCount() {
return classNameMatchingCount;
}

/**
* Set the classNameMatchingCount.
*
* @param classNameMatchingCount classNameMatchingCount
*/
public void setClassNameMatchingCount(int classNameMatchingCount) {
this.classNameMatchingCount = classNameMatchingCount;
}
}
}

0 comments on commit ed983f4

Please sign in to comment.