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 extends Annotation> accessorAnnotation();
+
+ protected abstract boolean isEligibleForTypeLevelAccessor(J.VariableDeclarations field,
+ J.VariableDeclarations.NamedVariable variable);
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ Class extends Annotation> accessorAnnotation = accessorAnnotation();
+ AnnotationMatcher accessorMatcher = new AnnotationMatcher("@" + accessorAnnotation.getName());
+ return new JavaIsoVisitor
+ * 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
+ * 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
+ * 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;
+ }
+ """
+ )
+ );
+ }
+}