Skip to content

Commit

Permalink
openrewrite#141 code review feedback, refactor to ScanningRecipe
Browse files Browse the repository at this point in the history
  • Loading branch information
kevin0x90 committed Oct 19, 2023
1 parent c03d6c6 commit 0f3ad83
Showing 1 changed file with 140 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.RemoveAnnotationVisitor;
import org.openrewrite.java.search.MaybeUsesImport;
import org.openrewrite.java.search.UsesJavaVersion;
import org.openrewrite.java.tree.*;

Expand All @@ -38,7 +39,14 @@

@Value
@EqualsAndHashCode(callSuper = false)
public class LombokValueToRecord extends Recipe {
public class LombokValueToRecord extends ScanningRecipe<Map<String, Set<String>>> {

private static final Pattern LOMBOK_ANNOTATION_PATTERN = Pattern.compile("^lombok.*");

private static final String LOMBOK_VALUE_IMPORT = "lombok.Value";

private static final String LOMBOK_VALUE_ANNOTATION = "@lombok.Value";


@Option(displayName = "useExactToString",
description = "When set the `toString` format from Lombok is used in the migrated record.")
Expand All @@ -51,7 +59,7 @@ public LombokValueToRecord(final @JsonProperty("useExactToString") boolean useEx

@Override
public String getDisplayName() {
return "Convert `@lombok.Value` class to Record";
return String.format("Convert `%s` class to Record", LOMBOK_VALUE_ANNOTATION);
}

@Override
Expand All @@ -65,27 +73,53 @@ public Set<String> getTags() {
}

@Override
public boolean causesAnotherCycle() {
return true;
public Map<String, Set<String>> getInitialValue(final ExecutionContext ctx) {
return new ConcurrentHashMap<>();
}

@Override
public int maxCycles() {
return 2;
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
public TreeVisitor<?, ExecutionContext> getScanner(final Map<String, Set<String>> acc) {
final TreeVisitor<?, ExecutionContext> check = Preconditions.and(
new UsesJavaVersion<>(17)
new UsesJavaVersion<>(17),
new MaybeUsesImport<>(LOMBOK_VALUE_IMPORT)
);

return Preconditions.check(check, new LombokValueToRecord.LombokValueToRecordVisitor(useExactToString));
return Preconditions.check(check, new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.ClassDeclaration visitClassDeclaration(
final J.ClassDeclaration classDecl,
final ExecutionContext executionContext
) {
final J.ClassDeclaration classDeclaration = super.visitClassDeclaration(classDecl, executionContext);

if (!isRelevantClass(classDeclaration)) {
return classDeclaration;
}

final List<J.VariableDeclarations> memberVariables = findAllClassFields(classDeclaration)
.collect(Collectors.toList());
if (hasMemberVariableAssignments(memberVariables)) {
return classDeclaration;
}

final JavaType.FullyQualified classType = requireNonNull(classDeclaration.getType(),
"Class type must not be null");

acc.putIfAbsent(
classType.getFullyQualifiedName(),
getMemberVariableNames(memberVariables));

return classDeclaration;
}
});
}

private static class LombokValueToRecordVisitor extends JavaIsoVisitor<ExecutionContext> {
@Override
public TreeVisitor<?, ExecutionContext> getVisitor(final Map<String, Set<String>> recordTypesToMembers) {
return new LombokValueToRecord.LombokValueToRecordVisitor(useExactToString, recordTypesToMembers);
}

private static final Pattern LOMBOK_ANNOTATION_PATTERN = Pattern.compile("^lombok.*");
private static class LombokValueToRecordVisitor extends JavaIsoVisitor<ExecutionContext> {

private static final JavaTemplate TO_STRING_TEMPLATE = JavaTemplate
.builder("@Override public String toString() { return \"#{}(\" +\n#{}\n\")\"; }")
Expand All @@ -96,14 +130,16 @@ private static class LombokValueToRecordVisitor extends JavaIsoVisitor<Execution

private static final String TO_STRING_MEMBER_DELIMITER = "\", \" +\n";

private static final Map<String, Set<String>> RECORD_TYPE_TO_MEMBERS = new ConcurrentHashMap<>();

private static final String STANDARD_GETTER_PREFIX = "get";

private final boolean useExactToString;

public LombokValueToRecordVisitor(final boolean useExactToString) {
private final Map<String, Set<String>> recordTypeToMembers;

public LombokValueToRecordVisitor(final boolean useExactToString,
final Map<String, Set<String>> recordTypeToMembers) {
this.useExactToString = useExactToString;
this.recordTypeToMembers = recordTypeToMembers;
}

@Override
Expand All @@ -113,7 +149,7 @@ public J.MethodInvocation visitMethodInvocation(
) {
final J.MethodInvocation methodInvocation = super.visitMethodInvocation(method, executionContext);

if (executionContext.getCycle() <= 1 || !isMethodInvocationOnRecordTypeClassMember(methodInvocation)) {
if (!isMethodInvocationOnRecordTypeClassMember(methodInvocation)) {
return methodInvocation;
}

Expand All @@ -124,7 +160,7 @@ public J.MethodInvocation visitMethodInvocation(
);
}

private static boolean isMethodInvocationOnRecordTypeClassMember(final J.MethodInvocation methodInvocation) {
private boolean isMethodInvocationOnRecordTypeClassMember(final J.MethodInvocation methodInvocation) {
final Expression expression = methodInvocation.getSelect();
if (!isClassExpression(expression)) {
return false;
Expand All @@ -136,12 +172,11 @@ private static boolean isMethodInvocationOnRecordTypeClassMember(final J.MethodI
}

final String methodName = methodInvocation.getName().getSimpleName();
final String classFqn = classType.getFullyQualifiedName();

return RECORD_TYPE_TO_MEMBERS.containsKey(classType.getFullyQualifiedName())
return recordTypeToMembers.containsKey(classFqn)
&& methodName.startsWith(STANDARD_GETTER_PREFIX)
&& RECORD_TYPE_TO_MEMBERS
.get(classType.getFullyQualifiedName())
.contains(getterMethodNameToFluentMethodName(methodName));
&& recordTypeToMembers.get(classFqn).contains(getterMethodNameToFluentMethodName(methodName));
}

private static boolean isClassExpression(final @Nullable Expression expression) {
Expand All @@ -165,16 +200,14 @@ private static String getterMethodNameToFluentMethodName(final String methodName
@Override
public J.ClassDeclaration visitClassDeclaration(final J.ClassDeclaration cd, final ExecutionContext ctx) {
J.ClassDeclaration classDeclaration = super.visitClassDeclaration(cd, ctx);
final JavaType.FullyQualified classType = classDeclaration.getType();

if (!isRelevantClass(classDeclaration)) {
if (classType == null || !recordTypeToMembers.containsKey(classType.getFullyQualifiedName())) {
return classDeclaration;
}

final List<J.VariableDeclarations> memberVariables = findAllClassFields(classDeclaration)
.collect(Collectors.toList());
if (hasMemberVariableAssignments(memberVariables)) {
return classDeclaration;
}

final List<Statement> bodyStatements = new ArrayList<>(classDeclaration.getBody().getStatements());
bodyStatements.removeAll(memberVariables);
Expand All @@ -192,8 +225,6 @@ public J.ClassDeclaration visitClassDeclaration(final J.ClassDeclaration cd, fin
classDeclaration = addExactToStringMethod(classDeclaration, memberVariables);
}

addToRecordTypeState(classDeclaration, memberVariables);

return maybeAutoFormat(cd, classDeclaration, ctx);
}

Expand All @@ -206,57 +237,13 @@ private J.ClassDeclaration addExactToStringMethod(final J.ClassDeclaration class
memberVariablesToString(getMemberVariableNames(memberVariables))));
}

private static boolean isRelevantClass(final J.ClassDeclaration classDeclaration) {
return classDeclaration.getType() != null
&& !isRecord(classDeclaration)
&& hasOnlyLombokValueAnnotation(classDeclaration)
&& !hasGenericTypeParameter(classDeclaration)
&& !hasExplicitMethods(classDeclaration)
&& !hasExplicitConstructor(classDeclaration)
&& !hasMemberVariableAnnotations(classDeclaration);
}

private static String memberVariablesToString(final Set<String> memberVariables) {
return memberVariables
.stream()
.map(member -> String.format(TO_STRING_MEMBER_LINE_PATTERN, member, member))
.collect(Collectors.joining(TO_STRING_MEMBER_DELIMITER));
}

private static void addToRecordTypeState(final J.ClassDeclaration classDeclaration,
final List<J.VariableDeclarations> memberVariables
) {
final JavaType.FullyQualified classType = requireNonNull(classDeclaration.getType(),
"Class type must not be null");

RECORD_TYPE_TO_MEMBERS.putIfAbsent(
classType.getFullyQualifiedName(),
getMemberVariableNames(memberVariables));
}

private static boolean hasExplicitMethods(final J.ClassDeclaration classDeclaration) {
return classDeclaration
.getBody()
.getStatements()
.stream()
.anyMatch(J.MethodDeclaration.class::isInstance);
}

private static Set<String> getMemberVariableNames(final List<J.VariableDeclarations> memberVariables) {
return memberVariables
.stream()
.map(J.VariableDeclarations::getVariables)
.flatMap(List::stream)
.map(J.VariableDeclarations.NamedVariable::getSimpleName)
.collect(LinkedHashSet::new, LinkedHashSet::add, LinkedHashSet::addAll);
}

private static boolean hasGenericTypeParameter(final J.ClassDeclaration classDeclaration) {
final List<J.TypeParameter> typeParameters = classDeclaration.getTypeParameters();

return typeParameters != null && !typeParameters.isEmpty();
}

private static JavaType.Class buildRecordType(final J.ClassDeclaration classDeclaration) {
requireNonNull(classDeclaration.getType(), "Class type must not be null");
final String className = requireNonNull(classDeclaration.getType().getFullyQualifiedName(),
Expand All @@ -266,28 +253,6 @@ private static JavaType.Class buildRecordType(final J.ClassDeclaration classDecl
.withKind(JavaType.FullyQualified.Kind.Record);
}

private static boolean isRecord(final J.ClassDeclaration classDeclaration) {
return J.ClassDeclaration.Kind.Type.Record.equals(classDeclaration.getKind());
}

private static boolean hasExplicitConstructor(final J.ClassDeclaration classDeclaration) {
return classDeclaration.getPrimaryConstructor() != null || classDeclaration
.getBody()
.getStatements()
.stream()
.filter(J.MethodDeclaration.class::isInstance)
.map(J.MethodDeclaration.class::cast)
.map(J.MethodDeclaration::getMethodType)
.filter(Objects::nonNull)
.anyMatch(JavaType.Method::isConstructor);
}

private static boolean hasMemberVariableAnnotations(final J.ClassDeclaration classDeclaration) {
return findAllClassFields(classDeclaration)
.map(J.VariableDeclarations::getAllAnnotations)
.anyMatch(annotations -> !annotations.isEmpty());
}

private static List<Statement> mapToConstructorArguments(
final List<J.VariableDeclarations> memberVariables
) {
Expand All @@ -302,35 +267,90 @@ private static List<Statement> mapToConstructorArguments(
}

private J.ClassDeclaration removeValueAnnotation(final J.ClassDeclaration cd, final ExecutionContext ctx) {
maybeRemoveImport("lombok.Value");
maybeRemoveImport(LOMBOK_VALUE_IMPORT);

return new RemoveAnnotationVisitor(
new AnnotationMatcher("@lombok.Value")
).visitClassDeclaration(cd, ctx);
return new RemoveAnnotationVisitor(new AnnotationMatcher(LOMBOK_VALUE_ANNOTATION))
.visitClassDeclaration(cd, ctx);
}
}

private static Stream<J.VariableDeclarations> findAllClassFields(final J.ClassDeclaration cd) {
return new ArrayList<>(cd.getBody().getStatements())
.stream()
.filter(J.VariableDeclarations.class::isInstance)
.map(J.VariableDeclarations.class::cast);
}
private static boolean hasMemberVariableAssignments(final List<J.VariableDeclarations> memberVariables) {
return memberVariables
.stream()
.map(J.VariableDeclarations::getVariables)
.flatMap(List::stream)
.map(J.VariableDeclarations.NamedVariable::getInitializer)
.anyMatch(J.Literal.class::isInstance);
}

private static boolean hasMemberVariableAssignments(final List<J.VariableDeclarations> memberVariables) {
return memberVariables
.stream()
.map(J.VariableDeclarations::getVariables)
.flatMap(List::stream)
.map(J.VariableDeclarations.NamedVariable::getInitializer)
.anyMatch(J.Literal.class::isInstance);
}
private static boolean isRelevantClass(final J.ClassDeclaration classDeclaration) {
return classDeclaration.getType() != null
&& !isRecord(classDeclaration)
&& hasOnlyLombokValueAnnotation(classDeclaration)
&& !hasGenericTypeParameter(classDeclaration)
&& !hasExplicitMethods(classDeclaration)
&& !hasExplicitConstructor(classDeclaration)
&& !hasMemberVariableAnnotations(classDeclaration);
}

private static boolean hasOnlyLombokValueAnnotation(final J.ClassDeclaration cd) {
return cd.getAllAnnotations()
.stream()
.filter(annotation -> TypeUtils.isAssignableTo(LOMBOK_ANNOTATION_PATTERN, annotation.getType()))
.map(J.Annotation::getSimpleName)
.allMatch("Value"::equals);
}
private static boolean isRecord(final J.ClassDeclaration classDeclaration) {
return J.ClassDeclaration.Kind.Type.Record.equals(classDeclaration.getKind());
}

private static boolean hasExplicitConstructor(final J.ClassDeclaration classDeclaration) {
return classDeclaration.getPrimaryConstructor() != null || classDeclaration
.getBody()
.getStatements()
.stream()
.filter(J.MethodDeclaration.class::isInstance)
.map(J.MethodDeclaration.class::cast)
.map(J.MethodDeclaration::getMethodType)
.filter(Objects::nonNull)
.anyMatch(JavaType.Method::isConstructor);
}

private static boolean hasGenericTypeParameter(final J.ClassDeclaration classDeclaration) {
final List<J.TypeParameter> typeParameters = classDeclaration.getTypeParameters();

return typeParameters != null && !typeParameters.isEmpty();
}

private static boolean hasExplicitMethods(final J.ClassDeclaration classDeclaration) {
return classDeclaration
.getBody()
.getStatements()
.stream()
.anyMatch(J.MethodDeclaration.class::isInstance);
}

private static boolean hasOnlyLombokValueAnnotation(final J.ClassDeclaration cd) {
return cd.getAllAnnotations()
.stream()
.filter(annotation -> TypeUtils.isAssignableTo(LOMBOK_ANNOTATION_PATTERN, annotation.getType()))
.map(J.Annotation::getSimpleName)
.allMatch("Value"::equals);
}

private static boolean hasMemberVariableAnnotations(final J.ClassDeclaration classDeclaration) {
return findAllClassFields(classDeclaration)
.map(J.VariableDeclarations::getAllAnnotations)
.anyMatch(annotations -> !annotations.isEmpty());
}

private static Stream<J.VariableDeclarations> findAllClassFields(final J.ClassDeclaration cd) {
return new ArrayList<>(cd.getBody().getStatements())
.stream()
.filter(J.VariableDeclarations.class::isInstance)
.map(J.VariableDeclarations.class::cast);
}

private static Set<String> getMemberVariableNames(final List<J.VariableDeclarations> memberVariables) {
return memberVariables
.stream()
.map(J.VariableDeclarations::getVariables)
.flatMap(List::stream)
.map(J.VariableDeclarations.NamedVariable::getSimpleName)
.collect(LinkedHashSet::new, LinkedHashSet::add, LinkedHashSet::addAll);
}
}

0 comments on commit 0f3ad83

Please sign in to comment.