diff --git a/src/main/java/org/openrewrite/java/migrate/lombok/LombokAccessorOnType.java b/src/main/java/org/openrewrite/java/migrate/lombok/LombokAccessorOnType.java new file mode 100644 index 0000000000..869aecc26c --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lombok/LombokAccessorOnType.java @@ -0,0 +1,166 @@ +/* + * Copyright 2026 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 org.jspecify.annotations.Nullable; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.AnnotationMatcher; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.tree.Flag; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.Statement; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static java.util.Comparator.comparing; + +abstract class LombokAccessorOnType extends Recipe { + + protected abstract Class accessorAnnotation(); + + protected abstract boolean isEligibleForTypeLevelAccessor(J.VariableDeclarations field, + J.VariableDeclarations.NamedVariable variable); + + @Override + public TreeVisitor getVisitor() { + Class accessorAnnotation = accessorAnnotation(); + AnnotationMatcher accessorMatcher = new AnnotationMatcher("@" + accessorAnnotation.getName()); + return new JavaIsoVisitor() { + private final Map> fieldsToHoistByClass = new HashMap<>(); + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + Set fieldsToHoist = fieldsToHoist(classDecl, accessorMatcher); + if (fieldsToHoist == null) { + return super.visitClassDeclaration(classDecl, ctx); + } + + fieldsToHoistByClass.put(classDecl.getId(), fieldsToHoist); + maybeAddImport(accessorAnnotation.getName()); + J.ClassDeclaration cd = JavaTemplate.builder("@" + accessorAnnotation.getSimpleName()) + .imports(accessorAnnotation.getName()) + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "lombok")) + .build() + .apply(getCursor(), classDecl.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName))); + return super.visitClassDeclaration(cd, ctx); + } + + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations variableDeclarations, + ExecutionContext ctx) { + J.VariableDeclarations vd = super.visitVariableDeclarations(variableDeclarations, ctx); + J.ClassDeclaration enclosing = getCursor().firstEnclosing(J.ClassDeclaration.class); + if (enclosing == null) { + return vd; + } + + Set fieldsToHoist = fieldsToHoistByClass.get(enclosing.getId()); + if (fieldsToHoist == null || !fieldsToHoist.contains(vd.getId())) { + return vd; + } + + List annotations = new ArrayList<>(vd.getLeadingAnnotations()); + annotations.removeIf(accessorMatcher::matches); + return maybeAutoFormat(vd, vd.withLeadingAnnotations(annotations), ctx); + } + }; + } + + private @Nullable Set fieldsToHoist(J.ClassDeclaration classDecl, AnnotationMatcher accessorMatcher) { + if (classDecl.getKind() != J.ClassDeclaration.Kind.Type.Class || + classDecl.getLeadingAnnotations().stream().anyMatch(accessorMatcher::matches)) { + return null; + } + + Set fieldsToHoist = new HashSet<>(); + for (Statement statement : classDecl.getBody().getStatements()) { + if (!(statement instanceof J.VariableDeclarations)) { + continue; + } + + J.VariableDeclarations field = (J.VariableDeclarations) statement; + boolean hasEligibleField = false; + boolean hasIneligibleField = false; + for (J.VariableDeclarations.NamedVariable variable : field.getVariables()) { + if (isEligibleForTypeLevelAccessor(field, variable)) { + hasEligibleField = true; + } else { + hasIneligibleField = true; + } + } + + // A single declaration can annotate multiple variables; do not remove an annotation partially. + if (hasEligibleField && hasIneligibleField) { + return null; + } + if (!hasEligibleField) { + continue; + } + + J.Annotation annotation = findAccessorAnnotation(field, accessorMatcher); + if (annotation == null || + (annotation.getArguments() != null && !annotation.getArguments().isEmpty())) { + return null; + } + fieldsToHoist.add(field.getId()); + } + return fieldsToHoist.isEmpty() ? null : fieldsToHoist; + } + + private static J.@Nullable Annotation findAccessorAnnotation(J.VariableDeclarations field, + AnnotationMatcher accessorMatcher) { + J.Annotation result = null; + for (J.Annotation annotation : field.getLeadingAnnotations()) { + if (accessorMatcher.matches(annotation)) { + if (result != null) { + return null; + } + result = annotation; + } + } + return result; + } + + protected static boolean isStaticField(J.VariableDeclarations field, + J.VariableDeclarations.NamedVariable variable) { + JavaType.Variable fieldType = variable.getName().getFieldType(); + return field.hasModifier(J.Modifier.Type.Static) || + (fieldType != null && fieldType.hasFlags(Flag.Static)); + } + + protected static boolean isFinalField(J.VariableDeclarations field, + J.VariableDeclarations.NamedVariable variable) { + JavaType.Variable fieldType = variable.getName().getFieldType(); + return field.hasModifier(J.Modifier.Type.Final) || + (fieldType != null && fieldType.hasFlags(Flag.Final)); + } + + protected static boolean hasSyntheticFieldName(J.VariableDeclarations.NamedVariable variable) { + return variable.getSimpleName().startsWith("$"); + } +} diff --git a/src/main/java/org/openrewrite/java/migrate/lombok/UseLombokGetterOnType.java b/src/main/java/org/openrewrite/java/migrate/lombok/UseLombokGetterOnType.java new file mode 100644 index 0000000000..812f052e65 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lombok/UseLombokGetterOnType.java @@ -0,0 +1,48 @@ +/* + * Copyright 2026 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.EqualsAndHashCode; +import lombok.Getter; +import lombok.Value; +import org.openrewrite.java.tree.J; + +import java.lang.annotation.Annotation; +import java.util.Set; + +import static java.util.Collections.singleton; + +@EqualsAndHashCode(callSuper = false) +@Value +public class UseLombokGetterOnType extends LombokAccessorOnType { + + String displayName = "Use class-level Lombok `@Getter` annotation"; + + String description = "Replace default field-level Lombok `@Getter` annotations with a class-level annotation when they apply to every eligible field."; + + Set tags = singleton("lombok"); + + @Override + protected Class accessorAnnotation() { + return Getter.class; + } + + @Override + protected boolean isEligibleForTypeLevelAccessor(J.VariableDeclarations field, + J.VariableDeclarations.NamedVariable variable) { + return !isStaticField(field, variable) && !hasSyntheticFieldName(variable); + } +} diff --git a/src/main/java/org/openrewrite/java/migrate/lombok/UseLombokSetterOnType.java b/src/main/java/org/openrewrite/java/migrate/lombok/UseLombokSetterOnType.java new file mode 100644 index 0000000000..1698d73cd1 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lombok/UseLombokSetterOnType.java @@ -0,0 +1,50 @@ +/* + * Copyright 2026 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.EqualsAndHashCode; +import lombok.Setter; +import lombok.Value; +import org.openrewrite.java.tree.J; + +import java.lang.annotation.Annotation; +import java.util.Set; + +import static java.util.Collections.singleton; + +@EqualsAndHashCode(callSuper = false) +@Value +public class UseLombokSetterOnType extends LombokAccessorOnType { + + String displayName = "Use class-level Lombok `@Setter` annotation"; + + String description = "Replace default field-level Lombok `@Setter` annotations with a class-level annotation when they apply to every eligible field."; + + Set tags = singleton("lombok"); + + @Override + protected Class accessorAnnotation() { + return Setter.class; + } + + @Override + protected boolean isEligibleForTypeLevelAccessor(J.VariableDeclarations field, + J.VariableDeclarations.NamedVariable variable) { + return !isStaticField(field, variable) && + !isFinalField(field, variable) && + !hasSyntheticFieldName(variable); + } +} diff --git a/src/main/resources/META-INF/rewrite/lombok.yml b/src/main/resources/META-INF/rewrite/lombok.yml index 8739fe42cd..cf1a40147b 100644 --- a/src/main/resources/META-INF/rewrite/lombok.yml +++ b/src/main/resources/META-INF/rewrite/lombok.yml @@ -26,6 +26,8 @@ recipeList: - org.openrewrite.java.migrate.lombok.log.UseLombokLogAnnotations - org.openrewrite.java.migrate.lombok.UseLombokGetter - org.openrewrite.java.migrate.lombok.UseLombokSetter + - org.openrewrite.java.migrate.lombok.UseLombokGetterOnType + - org.openrewrite.java.migrate.lombok.UseLombokSetterOnType - org.openrewrite.java.migrate.lombok.UseNoArgsConstructor - org.openrewrite.java.migrate.lombok.UseRequiredArgsConstructor - org.openrewrite.java.migrate.lombok.UseAllArgsConstructor diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index a25d96d967..903b821d75 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -429,7 +429,9 @@ maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.l maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UpdateLombokToJava11,Migrate Lombok to a Java 11 compatible version,Update Lombok dependency to a version that is compatible with Java 11 and migrate experimental Lombok types that have been promoted.,9,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UseAllArgsConstructor,Use `@AllArgsConstructor` where applicable,Prefer the Lombok `@AllArgsConstructor` annotation over explicitly written out constructors that assign all non-static fields.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UseLombokGetter,Convert getter methods to annotations,Convert trivial getter methods to `@Getter` annotations on their respective fields.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UseLombokGetterOnType,Use class-level Lombok `@Getter` annotation,Replace default field-level Lombok `@Getter` annotations with a class-level annotation when they apply to every eligible field.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UseLombokSetter,Convert setter methods to annotations,Convert trivial setter methods to `@Setter` annotations on their respective fields.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UseLombokSetterOnType,Use class-level Lombok `@Setter` annotation,Replace default field-level Lombok `@Setter` annotations with a class-level annotation when they apply to every eligible field.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UseNoArgsConstructor,Use `@NoArgsConstructor` where applicable,Prefer the Lombok `@NoArgsConstructor` annotation over explicitly written out constructors.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.UseRequiredArgsConstructor,Use `@RequiredArgsConstructor` where applicable,Prefer the Lombok `@RequiredArgsConstructor` annotation over explicitly written out constructors that only assign final fields.,1,,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lombok.log.UseCommonsLog,Use `@CommonsLog` instead of explicit fields,Prefer the lombok annotation `@CommonsLog` over explicitly written out `org.apache.commons.logging.Log` fields.,1,Log,Lombok,Modernize,Java,,Recipes for working with [Lombok](https://projectlombok.org/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,"[{""name"":""fieldName"",""type"":""String"",""displayName"":""Name of the log field"",""description"":""Name of the log field to replace. If not specified, the field name is not checked and any field that satisfies the other checks is converted."",""example"":""LOGGER""}]", diff --git a/src/test/java/org/openrewrite/java/migrate/lombok/UseLombokGetterOnTypeTest.java b/src/test/java/org/openrewrite/java/migrate/lombok/UseLombokGetterOnTypeTest.java new file mode 100644 index 0000000000..7acb0f5ebe --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/lombok/UseLombokGetterOnTypeTest.java @@ -0,0 +1,177 @@ +/* + * Copyright 2026 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 org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.Issue; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class UseLombokGetterOnTypeTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new UseLombokGetterOnType()) + .parser(JavaParser.fromJavaVersion().classpath("lombok")); + } + + @DocumentExample + @Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/1047") + @Test + void hoistsDefaultGetterAnnotations() { + rewriteRun( + //language=java + java( + """ + import lombok.Getter; + + class Person { + @Getter + private String name; + @Getter + private int age; + } + """, + """ + import lombok.Getter; + + @Getter + class Person { + private String name; + private int age; + } + """ + ) + ); + } + + @Test + void retainsStaticAndSyntheticFieldAnnotations() { + rewriteRun( + //language=java + java( + """ + import lombok.Getter; + + class Cache { + @Getter + private String value; + @Getter + private static String shared; + @Getter + private String $cachedValue; + } + """, + """ + import lombok.Getter; + + @Getter + class Cache { + private String value; + @Getter + private static String shared; + @Getter + private String $cachedValue; + } + """ + ) + ); + } + + @Test + void doesNotHoistWhenAnEligibleFieldLacksGetter() { + rewriteRun( + //language=java + java( + """ + import lombok.Getter; + + class Person { + @Getter + private String name; + private int age; + } + """ + ) + ); + } + + @Test + void doesNotHoistConfiguredGetterAnnotations() { + rewriteRun( + //language=java + java( + """ + import lombok.AccessLevel; + import lombok.Getter; + + class Person { + @Getter(AccessLevel.PACKAGE) + private String name; + } + """ + ) + ); + } + + @Test + void doesNotHoistMixedVariableDeclarations() { + rewriteRun( + //language=java + java( + """ + import lombok.Getter; + + class Cache { + @Getter + private String $cachedValue, value; + } + """ + ) + ); + } + + @Test + void hoistsGetterCreatedByLombokBestPractices() { + rewriteRun( + spec -> spec.recipeFromResources("org.openrewrite.java.migrate.lombok.LombokBestPractices"), + //language=java + java( + """ + class Person { + private String name; + + public String getName() { + return name; + } + } + """, + """ + import lombok.Getter; + + @Getter + class Person { + private String name; + } + """ + ) + ); + } +} diff --git a/src/test/java/org/openrewrite/java/migrate/lombok/UseLombokSetterOnTypeTest.java b/src/test/java/org/openrewrite/java/migrate/lombok/UseLombokSetterOnTypeTest.java new file mode 100644 index 0000000000..49ed2eb6df --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/lombok/UseLombokSetterOnTypeTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2026 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 org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.Issue; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class UseLombokSetterOnTypeTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new UseLombokSetterOnType()) + .parser(JavaParser.fromJavaVersion().classpath("lombok")); + } + + @DocumentExample + @Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/1047") + @Test + void hoistsDefaultSetterAnnotations() { + rewriteRun( + //language=java + java( + """ + import lombok.Setter; + + class Person { + @Setter + private String name; + @Setter + private int age; + } + """, + """ + import lombok.Setter; + + @Setter + class Person { + private String name; + private int age; + } + """ + ) + ); + } + + @Test + void retainsFieldsSkippedByTypeLevelSetter() { + rewriteRun( + //language=java + java( + """ + import lombok.Setter; + + class Cache { + @Setter + private String value; + @Setter + private static String shared; + @Setter + private final String id = "id"; + @Setter + private String $cachedValue; + } + """, + """ + import lombok.Setter; + + @Setter + class Cache { + private String value; + @Setter + private static String shared; + @Setter + private final String id = "id"; + @Setter + private String $cachedValue; + } + """ + ) + ); + } + + @Test + void doesNotHoistWhenAMutableFieldLacksSetter() { + rewriteRun( + //language=java + java( + """ + import lombok.Setter; + + class Person { + @Setter + private String name; + private int age; + } + """ + ) + ); + } + + @Test + void doesNotHoistConfiguredSetterAnnotations() { + rewriteRun( + //language=java + java( + """ + import lombok.AccessLevel; + import lombok.Setter; + + class Person { + @Setter(AccessLevel.PACKAGE) + private String name; + } + """ + ) + ); + } + + @Test + void doesNotHoistMixedVariableDeclarations() { + rewriteRun( + //language=java + java( + """ + import lombok.Setter; + + class Cache { + @Setter + private String $cachedValue, value; + } + """ + ) + ); + } +}