diff --git a/src/main/java/org/openrewrite/java/migrate/lombok/LombokValueToRecord.java b/src/main/java/org/openrewrite/java/migrate/lombok/LombokValueToRecord.java index db7947031d..5b0f79a913 100644 --- a/src/main/java/org/openrewrite/java/migrate/lombok/LombokValueToRecord.java +++ b/src/main/java/org/openrewrite/java/migrate/lombok/LombokValueToRecord.java @@ -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.*; @@ -38,7 +39,14 @@ @Value @EqualsAndHashCode(callSuper = false) -public class LombokValueToRecord extends Recipe { +public class LombokValueToRecord extends ScanningRecipe>> { + + 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.") @@ -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 @@ -65,27 +73,53 @@ public Set getTags() { } @Override - public boolean causesAnotherCycle() { - return true; + public Map> getInitialValue(final ExecutionContext ctx) { + return new ConcurrentHashMap<>(); } @Override - public int maxCycles() { - return 2; - } - - @Override - public TreeVisitor getVisitor() { + public TreeVisitor getScanner(final Map> acc) { final TreeVisitor 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() { + @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 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 { + @Override + public TreeVisitor getVisitor(final Map> recordTypesToMembers) { + return new LombokValueToRecord.LombokValueToRecordVisitor(useExactToString, recordTypesToMembers); + } - private static final Pattern LOMBOK_ANNOTATION_PATTERN = Pattern.compile("^lombok.*"); + private static class LombokValueToRecordVisitor extends JavaIsoVisitor { private static final JavaTemplate TO_STRING_TEMPLATE = JavaTemplate .builder("@Override public String toString() { return \"#{}(\" +\n#{}\n\")\"; }") @@ -96,14 +130,16 @@ private static class LombokValueToRecordVisitor extends JavaIsoVisitor> 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> recordTypeToMembers; + + public LombokValueToRecordVisitor(final boolean useExactToString, + final Map> recordTypeToMembers) { this.useExactToString = useExactToString; + this.recordTypeToMembers = recordTypeToMembers; } @Override @@ -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; } @@ -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; @@ -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) { @@ -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 memberVariables = findAllClassFields(classDeclaration) .collect(Collectors.toList()); - if (hasMemberVariableAssignments(memberVariables)) { - return classDeclaration; - } final List bodyStatements = new ArrayList<>(classDeclaration.getBody().getStatements()); bodyStatements.removeAll(memberVariables); @@ -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); } @@ -206,16 +237,6 @@ 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 memberVariables) { return memberVariables .stream() @@ -223,40 +244,6 @@ private static String memberVariablesToString(final Set memberVariables) .collect(Collectors.joining(TO_STRING_MEMBER_DELIMITER)); } - private static void addToRecordTypeState(final J.ClassDeclaration classDeclaration, - final List 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 getMemberVariableNames(final List 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 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(), @@ -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 mapToConstructorArguments( final List memberVariables ) { @@ -302,35 +267,90 @@ private static List 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 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 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 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 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 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 getMemberVariableNames(final List memberVariables) { + return memberVariables + .stream() + .map(J.VariableDeclarations::getVariables) + .flatMap(List::stream) + .map(J.VariableDeclarations.NamedVariable::getSimpleName) + .collect(LinkedHashSet::new, LinkedHashSet::add, LinkedHashSet::addAll); } } +