Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions src/main/java/org/openrewrite/staticanalysis/FinalClass.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,51 @@
*/
package org.openrewrite.staticanalysis;

import org.openrewrite.ExecutionContext;
import org.openrewrite.Preconditions;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.staticanalysis.java.JavaFileChecker;

import java.util.List;
import java.util.Set;

import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;

@EqualsAndHashCode(callSuper = false)
@Value
public class FinalClass extends Recipe {

@Option(displayName = "Include never-extended classes",
description = "Finalize classes that are never extended anywhere in the codebase",
required = false)
@Nullable
Boolean includeNeverExtended;

@Option(displayName = "Exclude packages",
description = "Package patterns to exclude from never-extended finalization (e.g., com.example.api.*)",
example = "com.example.api.*",
required = false)
@Nullable
List<String> excludePackages;

@Option(displayName = "Exclude annotations",
description = "Classes with these annotations won't be finalized when using never-extended mode",
example = "@ExtensionPoint",
required = false)
@Nullable
List<String> excludeAnnotations;

@Override
public String getDisplayName() {
return "Finalize classes with private constructors";
}

@Override
public String getDescription() {
return "Adds the `final` modifier to classes that expose no public or package-private constructors.";
return "Adds the `final` modifier to classes that expose no public or package-private constructors." +
"Optionally, can also finalize classes that are never extended anywhere in the codebase.";
}

@Override
Expand All @@ -43,6 +69,10 @@ public Set<String> getTags() {

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(new JavaFileChecker<>(), new FinalClassVisitor());
boolean includeNeverExtendedFlag = Boolean.TRUE.equals(includeNeverExtended);
List<String> excludePackagesList = excludePackages != null ? excludePackages : emptyList();
List<String> excludeAnnotationsList = excludeAnnotations != null ? excludeAnnotations : emptyList();
FinalClassVisitor visitor = new FinalClassVisitor(includeNeverExtendedFlag, excludePackagesList, excludeAnnotationsList);
return Preconditions.check(new JavaFileChecker<>(), visitor);
}
}
154 changes: 128 additions & 26 deletions src/main/java/org/openrewrite/staticanalysis/FinalClassVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,30 @@ public class FinalClassVisitor extends JavaIsoVisitor<ExecutionContext> {

final Set<String> typesToFinalize = new HashSet<>();
final Set<String> typesToNotFinalize = new HashSet<>();
private final boolean includeNeverExtended;
private final List<String> excludePackages;
private final List<String> excludeAnnotations;

public FinalClassVisitor() {
this(false, emptyList(), emptyList());
}

public FinalClassVisitor(boolean includeNeverExtended,
List<String> excludePackages,
List<String> excludeAnnotations) {
this.includeNeverExtended = includeNeverExtended;
this.excludePackages = excludePackages != null ? excludePackages : emptyList();
this.excludeAnnotations = excludeAnnotations != null ? excludeAnnotations : emptyList();
}

@Override
public @Nullable J visit(@Nullable Tree tree, ExecutionContext ctx) {
boolean root = false;
if (visitRoot == null && tree != null) {
visitRoot = tree;
root = true;
typesToFinalize.clear();
typesToNotFinalize.clear();
}
J result = super.visit(tree, ctx);
if (root) {
Expand All @@ -60,13 +77,47 @@ public class FinalClassVisitor extends JavaIsoVisitor<ExecutionContext> {
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDeclaration, ExecutionContext ctx) {
J.ClassDeclaration cd = super.visitClassDeclaration(classDeclaration, ctx);

if (cd.getType() != null) {
excludeSupertypes(cd.getType());
}

if (cd.getKind() != J.ClassDeclaration.Kind.Type.Class || cd.hasModifier(J.Modifier.Type.Abstract) ||
cd.hasModifier(J.Modifier.Type.Final) || cd.getType() == null) {
return cd;
}

excludeSupertypes(cd.getType());
if (cd.hasModifier(J.Modifier.Type.Sealed) || cd.hasModifier(J.Modifier.Type.NonSealed)) {
return cd;
}

if (!includeNeverExtended) {
boolean allPrivate = true;
int constructorCount = 0;
for (Statement s : cd.getBody().getStatements()) {
if (s instanceof J.MethodDeclaration && ((J.MethodDeclaration) s).isConstructor()) {
J.MethodDeclaration constructor = (J.MethodDeclaration) s;
constructorCount++;
if (!constructor.hasModifier(J.Modifier.Type.Private)) {
allPrivate = false;
}
}
if (constructorCount > 0 && !allPrivate) {
return cd;
}
}

if (constructorCount > 0 && passesExclusionFilters(cd)) {
typesToFinalize.add(cd.getType().getFullyQualifiedName());
}
} else {
handleExtendedFinalization(cd);
}

return cd;
}

private void handleExtendedFinalization(J.ClassDeclaration cd) {
String fullyQualifiedName = cd.getType().getFullyQualifiedName();
boolean allPrivate = true;
int constructorCount = 0;
for (Statement s : cd.getBody().getStatements()) {
Expand All @@ -78,20 +129,68 @@ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDeclarat
}
}
if (constructorCount > 0 && !allPrivate) {
return cd;
break;
}
}

if (constructorCount > 0) {
typesToFinalize.add(cd.getType().getFullyQualifiedName());
if (constructorCount > 0 && allPrivate) {
typesToFinalize.add(fullyQualifiedName);
}

return cd;
if (passesExclusionFilters(cd)) {
typesToFinalize.add(fullyQualifiedName);
}
}

private boolean passesExclusionFilters(J.ClassDeclaration cd) {
if (cd.getType() == null) {
return false;
}

if (isPackageExcluded(cd.getType().getFullyQualifiedName())) {
return false;
}

return !hasExcludedAnnotation(cd);
}

private boolean isPackageExcluded(String className) {
return excludePackages.stream()
.anyMatch(pattern -> matchesPattern(className, pattern));
}

private boolean matchesPattern(String className, String pattern) {
if (pattern.endsWith("*")) {
String prefix = pattern.substring(0, pattern.length() - 1);
return className.startsWith(prefix);
}
return className.equals(pattern);
}

private boolean hasExcludedAnnotation(J.ClassDeclaration classDecl) {
return classDecl.getLeadingAnnotations().stream()
.anyMatch(annotation -> {
String annotationName = annotation.getAnnotationType().toString();
if (excludeAnnotations.contains(annotationName) || excludeAnnotations.contains("@" + annotationName)) {
return true;
}

// Check simple name (e.g., "Configuration" for "org.springframework.context.annotation.Configuration")
String simpleName = getSimpleName(annotationName);
if (excludeAnnotations.contains(simpleName) || excludeAnnotations.contains("@" + simpleName)) {
return true;
}
return false;
});
}

private String getSimpleName(String fullyQualifiedName) {
int lastDot = fullyQualifiedName.lastIndexOf('.');
return lastDot >= 0 ? fullyQualifiedName.substring(lastDot + 1) : fullyQualifiedName;
}

private void excludeSupertypes(JavaType.FullyQualified type) {
if (type.getSupertype() != null && type.getOwningClass() != null &&
typesToNotFinalize.add(type.getSupertype().getFullyQualifiedName())) {
if (type.getSupertype() != null && typesToNotFinalize.add(type.getSupertype().getFullyQualifiedName())) {
excludeSupertypes(type.getSupertype());
}
}
Expand All @@ -104,31 +203,34 @@ private static class FinalizingVisitor extends JavaIsoVisitor<ExecutionContext>
private final Set<String> typesToFinalize;

public FinalizingVisitor(Set<String> typesToFinalize) {
this.typesToFinalize = typesToFinalize;
this.typesToFinalize = new HashSet<>(typesToFinalize);
}

@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
if (cd.getType() != null && typesToFinalize.remove(cd.getType().getFullyQualifiedName())) {
List<J.Modifier> modifiers = new ArrayList<>(cd.getModifiers());
modifiers.add(new J.Modifier(randomId(), Space.EMPTY, Markers.EMPTY, null, J.Modifier.Type.Final, emptyList()));
modifiers = sortModifiers(modifiers);
cd = cd.withModifiers(modifiers);
if (cd.getType() instanceof JavaType.Class && !cd.getType().hasFlags(Flag.Final)) {
Set<Flag> flags = new HashSet<>(cd.getType().getFlags());
flags.add(Flag.Final);
cd = cd.withType(((JavaType.Class) cd.getType()).withFlags(flags));
}

// Temporary work around until issue https://github.com/openrewrite/rewrite/issues/2348 is implemented.
if (!cd.getLeadingAnnotations().isEmpty()) {
// Setting the prefix to empty will cause the `Spaces` visitor to fix the formatting.
cd = cd.getPadding().withKind(cd.getPadding().getKind().withPrefix(Space.EMPTY));
if (cd.getType() != null) {
String fullyQualifiedName = cd.getType().getFullyQualifiedName();
if (typesToFinalize.remove(fullyQualifiedName)) {
List<J.Modifier> modifiers = new ArrayList<>(cd.getModifiers());
modifiers.add(new J.Modifier(randomId(), Space.EMPTY, Markers.EMPTY, null, J.Modifier.Type.Final, emptyList()));
modifiers = sortModifiers(modifiers);
cd = cd.withModifiers(modifiers);
if (cd.getType() instanceof JavaType.Class && !cd.getType().hasFlags(Flag.Final)) {
Set<Flag> flags = new HashSet<>(cd.getType().getFlags());
flags.add(Flag.Final);
cd = cd.withType(((JavaType.Class) cd.getType()).withFlags(flags));
}

// Temporary work around until issue https://github.com/openrewrite/rewrite/issues/2348 is implemented.
if (!cd.getLeadingAnnotations().isEmpty()) {
// Setting the prefix to empty will cause the `Spaces` visitor to fix the formatting.
cd = cd.getPadding().withKind(cd.getPadding().getKind().withPrefix(Space.EMPTY));
}

assert getCursor().getParent() != null;
cd = autoFormat(cd, cd.getName(), ctx, getCursor().getParent());
}

assert getCursor().getParent() != null;
cd = autoFormat(cd, cd.getName(), ctx, getCursor().getParent());
}
return cd;
}
Expand Down
Loading