diff --git a/src/main/java/org/openrewrite/java/migrate/lombok/NegligentlyConvertEquals.java b/src/main/java/org/openrewrite/java/migrate/lombok/NegligentlyConvertEquals.java new file mode 100644 index 0000000000..f0e4024cb3 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lombok/NegligentlyConvertEquals.java @@ -0,0 +1,100 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.java.migrate.lombok; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.J; + +import static java.util.Comparator.comparing; + +@Value +@EqualsAndHashCode(callSuper = false) +public class NegligentlyConvertEquals extends Recipe { + + @Override + public String getDisplayName() { + //language=markdown + return "Replace any custom `equals` or `hashCode` methods with the `EqualsAndHashCode` annotation"; + } + + @Override + public String getDescription() { + //language=markdown + return "This recipe substitutes a class level `@EqualsAndHashCode` annotation for a custom `equals` or `hashCode` methods. " + + "If both are defined, then both will be replaced. If only one is defined then it will be replaced. " + + "This recipe does not check if the custom `equals` or `hashCode` methods behave like ones generated by the lombok annotation. " + + "Doing so is considered infeasible at this time. " + + "As a compromise this recipe finds and replaces the custom methods and relies on the user to review the changes closely. " + + "As a consequence this recipe is VERY DANGEROUS to include into a composite recipe! " + + "Users are advised to run it only in isolation."; + } + + @Override + public TreeVisitor getVisitor() { + return new Converter(); + } + + @Value + @EqualsAndHashCode(callSuper = false) + private static class Converter extends JavaIsoVisitor { + + MethodMatcher equalsMatcher = new MethodMatcher("* equals(Object)"); + MethodMatcher hashCodeMatcher = new MethodMatcher("* hashCode()"); + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + + J.ClassDeclaration classDeclAfterVisit = super.visitClassDeclaration(classDecl, ctx); + + //only thing that can have changed is removal of either equals or hash code + //and something needs to have changed before we add an annotation at class level + if (classDeclAfterVisit != classDecl) { + maybeAddImport("lombok.EqualsAndHashCode"); + + //Add annotation + JavaTemplate template = JavaTemplate.builder("@EqualsAndHashCode\n") + .imports("lombok.EqualsAndHashCode") + .javaParser(JavaParser.fromJavaVersion().classpath("lombok")) + .build(); + + return template.apply( + updateCursor(classDeclAfterVisit), + classDeclAfterVisit.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName))); + } + return classDecl; + } + + @Override + public J.@Nullable MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + J.ClassDeclaration classDecl = getCursor().firstEnclosingOrThrow(J.ClassDeclaration.class); + + // The enclosing class of a J.MethodDeclaration must be known for a MethodMatcher to match it + if (equalsMatcher.matches(method, classDecl) || hashCodeMatcher.matches(method, classDecl)) { + return null; + } + return method; + } + } +} diff --git a/src/test/java/org/openrewrite/java/migrate/lombok/NegligentlyConvertEqualsTest.java b/src/test/java/org/openrewrite/java/migrate/lombok/NegligentlyConvertEqualsTest.java new file mode 100644 index 0000000000..79f74cc883 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/lombok/NegligentlyConvertEqualsTest.java @@ -0,0 +1,181 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.java.migrate.lombok; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class NegligentlyConvertEqualsTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new NegligentlyConvertEquals()) + .parser(JavaParser.fromJavaVersion().logCompilationWarningsAndErrors(true).classpath("lombok")); + } + + @DocumentExample + @Test + void replaceEquals() { + rewriteRun(// language=java + java( + """ + class A { + + int foo; + + @Override + public boolean equals(Object o) { + return false; + } + } + """, + """ + import lombok.EqualsAndHashCode; + + @EqualsAndHashCode + class A { + + int foo; + } + """ + ) + ); + } + + @Test + void noCostomMethodsNoAnnotation() { + rewriteRun(// language=java + java( + """ + class A { + + int foo; + + } + """ + ) + ); + } + + @Test + void replaceEqualsInPackage() { + rewriteRun(// language=java + java( + """ + package com.example; + + class A { + + int foo; + + @Override + public boolean equals(Object o) { + return false; + } + } + """, + """ + package com.example; + + import lombok.EqualsAndHashCode; + + @EqualsAndHashCode + class A { + + int foo; + } + """ + ) + ); + } + + @Test + void replaceHashCode() { + rewriteRun(// language=java + java( + """ + package com.example; + + class A { + + int foo; + + @Override + public int hashCode() { + return 6; + } + } + """, + """ + package com.example; + + import lombok.EqualsAndHashCode; + + @EqualsAndHashCode + class A { + + int foo; + } + """ + ) + ); + } + + @Test + void replaceEqualsAndHashCode() { + rewriteRun(// language=java + java( + """ + package com.example; + + class A { + + int foo; + + @Override + public boolean equals(Object o) { + return false; + } + + @Override + public int hashCode() { + return 6; + } + + } + """, + """ + package com.example; + + import lombok.EqualsAndHashCode; + + @EqualsAndHashCode + class A { + + int foo; + + } + """ + ) + ); + } + +}