diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000000..9841008d7e --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(./gradlew test:*)", + "Bash(find:*)", + "WebFetch(domain:github.com)", + "Bash(git checkout:*)", + "Bash(git add:*)", + "Bash(git commit:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/src/main/java/org/openrewrite/java/migrate/lombok/LombokOnXToOnX_.java b/src/main/java/org/openrewrite/java/migrate/lombok/LombokOnXToOnX_.java new file mode 100644 index 0000000000..7052490aaf --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lombok/LombokOnXToOnX_.java @@ -0,0 +1,115 @@ +/* + * 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 org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.AnnotationMatcher; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; + +import java.util.Collections; +import java.util.Set; + +public class LombokOnXToOnX_ extends Recipe { + + private static final AnnotationMatcher LOMBOK_GETTER = new AnnotationMatcher("@lombok.Getter"); + private static final AnnotationMatcher LOMBOK_SETTER = new AnnotationMatcher("@lombok.Setter"); + private static final AnnotationMatcher LOMBOK_WITH = new AnnotationMatcher("@lombok.With"); + private static final AnnotationMatcher LOMBOK_WITHER = new AnnotationMatcher("@lombok.Wither"); + private static final AnnotationMatcher LOMBOK_EQUALS_AND_HASHCODE = new AnnotationMatcher("@lombok.EqualsAndHashCode"); + private static final AnnotationMatcher LOMBOK_TO_STRING = new AnnotationMatcher("@lombok.ToString"); + private static final AnnotationMatcher LOMBOK_REQUIRED_ARGS_CONSTRUCTOR = new AnnotationMatcher("@lombok.RequiredArgsConstructor"); + private static final AnnotationMatcher LOMBOK_ALL_ARGS_CONSTRUCTOR = new AnnotationMatcher("@lombok.AllArgsConstructor"); + private static final AnnotationMatcher LOMBOK_NO_ARGS_CONSTRUCTOR = new AnnotationMatcher("@lombok.NoArgsConstructor"); + + @Override + public String getDisplayName() { + return "Migrate Lombok's `@__` syntax to `onX_` for Java 8+"; + } + + @Override + public String getDescription() { + return "Migrates Lombok's `onX` annotations from the Java 7 style using `@__` to the Java 8+ style " + + "using `onX_`. For example, `@Getter(onMethod=@__({@Id}))` becomes `@Getter(onMethod_={@Id})`."; + } + + @Override + public Set getTags() { + return Collections.singleton("lombok"); + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + new UsesType<>("lombok.*", false), + new JavaIsoVisitor() { + @Override + public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) { + J.Annotation a = super.visitAnnotation(annotation, ctx); + + if (isLombokAnnotationWithOnX(a) && a.getArguments() != null && !a.getArguments().isEmpty()) { + a = a.withArguments(ListUtils.map(a.getArguments(), arg -> { + if (arg instanceof J.Assignment) { + J.Assignment assignment = (J.Assignment) arg; + if (assignment.getVariable() instanceof J.Identifier) { + J.Identifier id = (J.Identifier) assignment.getVariable(); + String name = id.getSimpleName(); + if (("onMethod".equals(name) || + "onParam".equals(name) || + "onConstructor".equals(name)) && + assignment.getAssignment() instanceof J.Annotation) { + J.Annotation onXAnnotation = (J.Annotation) assignment.getAssignment(); + if ("__".equals(onXAnnotation.getSimpleName()) && + onXAnnotation.getArguments() != null && + !onXAnnotation.getArguments().isEmpty()) { + // Change onMethod to onMethod_, onParam to onParam_, etc. + J.Identifier on_ = id.withSimpleName(name + "_"); + // If there's exactly one argument, use it directly + Expression newValue = onXAnnotation.getArguments().get(0); + return assignment.withVariable(on_).withAssignment(newValue); + } + } + } + } + return arg; + })); + } + + return a; + } + + private boolean isLombokAnnotationWithOnX(J.Annotation annotation) { + return LOMBOK_GETTER.matches(annotation) || + LOMBOK_SETTER.matches(annotation) || + LOMBOK_WITH.matches(annotation) || + LOMBOK_WITHER.matches(annotation) || + LOMBOK_EQUALS_AND_HASHCODE.matches(annotation) || + LOMBOK_TO_STRING.matches(annotation) || + LOMBOK_REQUIRED_ARGS_CONSTRUCTOR.matches(annotation) || + LOMBOK_ALL_ARGS_CONSTRUCTOR.matches(annotation) || + LOMBOK_NO_ARGS_CONSTRUCTOR.matches(annotation); + } + } + ); + } + +} diff --git a/src/main/resources/META-INF/rewrite/lombok.yml b/src/main/resources/META-INF/rewrite/lombok.yml index 9e384f14bf..a8b1606567 100644 --- a/src/main/resources/META-INF/rewrite/lombok.yml +++ b/src/main/resources/META-INF/rewrite/lombok.yml @@ -65,6 +65,7 @@ recipeList: oldFullyQualifiedTypeName: lombok.experimental.val newFullyQualifiedTypeName: lombok.val - org.openrewrite.java.migrate.lombok.LombokValToFinalVar + - org.openrewrite.java.migrate.lombok.LombokOnXToOnX_ --- type: specs.openrewrite.org/v1beta/recipe diff --git a/src/test/java/org/openrewrite/java/migrate/lombok/LombokOnXToOnX_Test.java b/src/test/java/org/openrewrite/java/migrate/lombok/LombokOnXToOnX_Test.java new file mode 100644 index 0000000000..6a36d8a1e3 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/lombok/LombokOnXToOnX_Test.java @@ -0,0 +1,219 @@ +/* + * 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 org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.ExpectedToFail; +import org.openrewrite.DocumentExample; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; + +import static org.openrewrite.java.Assertions.java; + +class LombokOnXToOnX_Test implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new LombokOnXToOnX_()) + .parser(JavaParser.fromJavaVersion().classpath("lombok")) + .typeValidationOptions(TypeValidation.all().identifiers(false)); + } + + @DocumentExample + @Test + void migrateGetterOnMethod() { + //language=java + rewriteRun( + java( + """ + import lombok.Getter; + class Example { + @Getter(onMethod=@__({@Deprecated})) + private String field; + } + """, + """ + import lombok.Getter; + class Example { + @Getter(onMethod_={@Deprecated}) + private String field; + } + """ + ) + ); + } + + @Test + void migrateSetterOnParam() { + //language=java + rewriteRun( + java( + """ + import lombok.Setter; + class Example { + @Setter(onParam=@__({@SuppressWarnings("unchecked")})) + private long unid; + } + """, + """ + import lombok.Setter; + class Example { + @Setter(onParam_={@SuppressWarnings("unchecked")}) + private long unid; + } + """ + ) + ); + } + + @Test + void migrateMultipleAnnotations() { + //language=java + rewriteRun( + java( + """ + import lombok.Getter; + import lombok.Setter; + class Example { + @Getter(onMethod=@__({@Deprecated, @SuppressWarnings("all")})) + @Setter(onParam=@__({@SuppressWarnings("unchecked")})) + private long unid; + } + """, + """ + import lombok.Getter; + import lombok.Setter; + class Example { + @Getter(onMethod_={@Deprecated, @SuppressWarnings("all")}) + @Setter(onParam_={@SuppressWarnings("unchecked")}) + private long unid; + } + """ + ) + ); + } + + @Test + void migrateConstructorAnnotations() { + //language=java + rewriteRun( + java( + """ + import lombok.RequiredArgsConstructor; + @RequiredArgsConstructor(onConstructor=@__({@Deprecated})) + class Example { + private final String field; + } + """, + """ + import lombok.RequiredArgsConstructor; + @RequiredArgsConstructor(onConstructor_={@Deprecated}) + class Example { + private final String field; + } + """ + ) + ); + } + + @Test + void doNotChangeIfAlreadyMigrated() { + //language=java + rewriteRun( + java( + """ + import lombok.Getter; + class Example { + @Getter(onMethod_={@Deprecated}) + private String field; + } + """ + ) + ); + } + + @Test + void doNotChangeIfNoOnXParameter() { + //language=java + rewriteRun( + java( + """ + import lombok.Getter; + class Example { + @Getter(lazy=true) + private final String field = "value"; + } + """ + ) + ); + } + + @Test + void handleEmptyAnnotationList() { + //language=java + rewriteRun( + java( + """ + import lombok.Getter; + class Example { + @Getter(onMethod=@__({})) + private String field; + } + """, + """ + import lombok.Getter; + class Example { + @Getter(onMethod_={}) + private String field; + } + """ + ) + ); + } + + @ExpectedToFail("Parser bug with @__ syntax causes test failure") + @Test + void handleWithAnnotation() { + //language=java + rewriteRun( + java( + """ + import lombok.With; + class Example { + @With(onParam=@__({@SuppressWarnings("unused")})) + private final String field; + + Example(String field) { + this.field = field; + } + } + """, + """ + import lombok.With; + class Example { + @With(onParam_={@SuppressWarnings("unused")}) + private final String field; + + Example(String field) { + this.field = field; + } + } + """ + ) + ); + } +}