diff --git a/src/main/java/org/openrewrite/java/migrate/lombok/UseNoArgsConstructor.java b/src/main/java/org/openrewrite/java/migrate/lombok/UseNoArgsConstructor.java new file mode 100644 index 0000000000..6300414109 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lombok/UseNoArgsConstructor.java @@ -0,0 +1,82 @@ +/* + * Copyright 2025 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.java.migrate.lombok; + +import lombok.AccessLevel; +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.tree.J; +import org.openrewrite.java.tree.TypeUtils; + +import static java.util.Comparator.comparing; + +@Value +@EqualsAndHashCode(callSuper = false) +public class UseNoArgsConstructor extends Recipe { + + @Override + public String getDisplayName() { + //language=markdown + return "Use `@NoArgsConstructor` where applicable"; + } + + @Override + public String getDescription() { + //language=markdown + return "Prefer the Lombok `@NoArgsConstructor` annotation over explicitly written out constructors."; + } + + @Override + public TreeVisitor getVisitor() { + return new JavaIsoVisitor() { + @Override + public J.@Nullable MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + if (method.isConstructor() && + method.getParameters().get(0) instanceof J.Empty && + method.getBody() != null && method.getBody().getStatements().isEmpty()) { + J.ClassDeclaration enclosing = getCursor().firstEnclosing(J.ClassDeclaration.class); + AccessLevel accessLevel = LombokUtils.getAccessLevel(method); + doAfterVisit(new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + if (TypeUtils.isOfType(classDecl.getType(), enclosing.getType())) { + String template = "@NoArgsConstructor" + (accessLevel == AccessLevel.PUBLIC ? + "" : "(access = AccessLevel." + accessLevel.name() + ")"); + maybeAddImport("lombok.AccessLevel"); + maybeAddImport("lombok.NoArgsConstructor"); + return JavaTemplate.builder(template) + .imports("lombok.*") + .javaParser(JavaParser.fromJavaVersion().classpath("lombok")) + .build() + .apply(getCursor(), classDecl.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName))); + } + return super.visitClassDeclaration(classDecl, ctx); + } + }); + return null; + } + return super.visitMethodDeclaration(method, ctx); + } + }; + } +} diff --git a/src/test/java/org/openrewrite/java/migrate/lombok/UseNoArgsConstructorTest.java b/src/test/java/org/openrewrite/java/migrate/lombok/UseNoArgsConstructorTest.java new file mode 100644 index 0000000000..92ec7a7024 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/lombok/UseNoArgsConstructorTest.java @@ -0,0 +1,138 @@ +/* + * 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.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class UseNoArgsConstructorTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new UseNoArgsConstructor()); + } + + @DocumentExample + @Test + void replaceEmptyPublicConstructor() { + rewriteRun(// language=java + java( + """ + class A { + public A() {} + } + """, + """ + import lombok.NoArgsConstructor; + + @NoArgsConstructor + class A { + } + """ + ) + ); + } + + @Test + void keepNonEmptyPublicConstructor() { + rewriteRun( + //language=java + java( + """ + class A { + + int foo; + + public A() { + foo = 7; + } + } + """ + ) + ); + } + + @Test + void replaceEmptyProtectedConstructor() { + rewriteRun( + //language=java + java( + """ + class A { + protected A() {} + } + """, + """ + import lombok.AccessLevel; + import lombok.NoArgsConstructor; + + @NoArgsConstructor(access = AccessLevel.PROTECTED) + class A { + } + """ + ) + ); + } + + @Test + void replaceEmptyPrivateConstructor() { + rewriteRun( + //language=java + java( + """ + class A { + private A() {} + } + """, + """ + import lombok.AccessLevel; + import lombok.NoArgsConstructor; + + @NoArgsConstructor(access = AccessLevel.PRIVATE) + class A { + } + """ + ) + ); + } + + @Test + void replaceEmptyPackageConstructor() { + rewriteRun( + //language=java + java( + """ + class A { + A() {} + } + """, + """ + import lombok.AccessLevel; + import lombok.NoArgsConstructor; + + @NoArgsConstructor(access = AccessLevel.PACKAGE) + class A { + } + """ + ) + ); + } + +}