org.openrewrite.staticanalysis.FinalizePrivateFields Maven / Gradle / Ivy
Show all versions of rewrite-static-analysis Show documentation
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://docs.moderne.io/licensing/moderne-source-available-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.staticanalysis;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.csharp.tree.Cs;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.service.AnnotationService;
import org.openrewrite.java.tree.*;
import org.openrewrite.marker.Markers;
import java.time.Duration;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public class FinalizePrivateFields extends Recipe {
@Override
public String getDisplayName() {
return "Finalize private fields";
}
@Override
public String getDescription() {
return "Adds the `final` modifier keyword to private instance variables which are not reassigned.";
}
@Override
public @Nullable Duration getEstimatedEffortPerOccurrence() {
return Duration.ofMinutes(2);
}
@Override
public TreeVisitor, ExecutionContext> getVisitor() {
return new JavaIsoVisitor() {
private Set privateFieldsToBeFinalized = new HashSet<>();
@Nullable
private SourceFile topLevel;
@Override
public @Nullable J visit(@Nullable Tree tree, ExecutionContext ctx) {
if (topLevel == null && tree instanceof SourceFile) {
topLevel = (SourceFile) tree;
}
return super.visit(tree, ctx);
}
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
if (!service(AnnotationService.class).getAllAnnotations(getCursor()).isEmpty()) {
// skip if a class has any annotation, since the annotation could generate some code to assign
// fields like Lombok @Setter.
return classDecl;
}
// skip if a class has multi constructor methods
if (getConstructorCount(classDecl) > 1 || isInnerClass(classDecl)) {
return classDecl;
}
List privateFields = collectPrivateFields(getCursor());
Map privateFieldAssignCountMap = privateFields.stream()
.filter(v -> v.getVariableType() != null)
.collect(Collectors.toMap(J.VariableDeclarations.NamedVariable::getVariableType,
v -> v.getInitializer() != null ? 1 : 0));
CollectPrivateFieldsAssignmentCounts.collect(classDecl, privateFieldAssignCountMap);
privateFieldsToBeFinalized = privateFieldAssignCountMap.entrySet()
.stream()
.filter(entry -> entry.getValue() == 1)
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
return super.visitClassDeclaration(classDecl, ctx);
}
@Override
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable,
ExecutionContext ctx) {
J.VariableDeclarations mv = super.visitVariableDeclarations(multiVariable, ctx);
boolean canAllVariablesBeFinalized = mv.getVariables()
.stream()
.map(J.VariableDeclarations.NamedVariable::getVariableType)
.allMatch(privateFieldsToBeFinalized::contains);
if (canAllVariablesBeFinalized) {
mv = autoFormat(mv.withVariables(ListUtils.map(mv.getVariables(), v -> {
JavaType.Variable type = v.getVariableType();
return type != null ? v.withVariableType(type.withFlags(
Flag.bitMapToFlags(type.getFlagsBitMap() | Flag.Final.getBitMask()))) : null;
})).withModifiers(ListUtils.concat(mv.getModifiers(),
new J.Modifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, topLevel instanceof Cs ? "readonly" : "final",
topLevel instanceof Cs ? J.Modifier.Type.LanguageExtension : J.Modifier.Type.Final, emptyList()))), ctx);
}
return mv;
}
private boolean anyAnnotationApplied(Cursor variableCursor) {
return !service(AnnotationService.class).getAllAnnotations(variableCursor).isEmpty() ||
variableCursor.getValue().getTypeExpression() instanceof J.AnnotatedType;
}
/**
* Collect private and non-final fields from a class
*/
private List collectPrivateFields(Cursor classCursor) {
J.ClassDeclaration classDecl = classCursor.getValue();
Cursor bodyCursor = new Cursor(classCursor, classDecl.getBody());
return classDecl.getBody()
.getStatements()
.stream()
.filter(J.VariableDeclarations.class::isInstance)
.map(J.VariableDeclarations.class::cast)
.filter(mv -> mv.hasModifier(J.Modifier.Type.Private) &&
!mv.hasModifier(J.Modifier.Type.Final) &&
(!(topLevel instanceof Cs) || mv.getModifiers().stream().noneMatch(m -> "readonly".equals(m.getKeyword()) || "const".equals(m.getKeyword()))) &&
!mv.hasModifier(J.Modifier.Type.Volatile))
.filter(mv -> !anyAnnotationApplied(new Cursor(bodyCursor, mv)))
.map(J.VariableDeclarations::getVariables)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
};
}
private static int getConstructorCount(J.ClassDeclaration classDecl) {
return (int) classDecl.getBody()
.getStatements()
.stream()
.filter(J.MethodDeclaration.class::isInstance)
.map(J.MethodDeclaration.class::cast)
.filter(J.MethodDeclaration::isConstructor)
.count();
}
private static boolean isInnerClass(J.ClassDeclaration classDecl) {
return classDecl.getType() != null && classDecl.getType().getOwningClass() != null;
}
private static class CollectPrivateFieldsAssignmentCounts extends JavaIsoVisitor