From f37beecb3377233eb0ca2f1d15209d1fb3def123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Merlin=20B=C3=B6gershausen?= Date: Tue, 28 May 2024 22:23:24 +0200 Subject: [PATCH 1/2] Add general recipe to replace an annotation with another based on an annotation matcher and JavaTemplate --- .../openrewrite/java/ReplaceAnnotation.java | 97 ++++++++++++ .../java/ReplaceAnnotationTest.java | 146 ++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 rewrite-java/src/main/java/org/openrewrite/java/ReplaceAnnotation.java create mode 100644 rewrite-java/src/test/java/org/openrewrite/java/ReplaceAnnotationTest.java diff --git a/rewrite-java/src/main/java/org/openrewrite/java/ReplaceAnnotation.java b/rewrite-java/src/main/java/org/openrewrite/java/ReplaceAnnotation.java new file mode 100644 index 00000000000..ec731804b52 --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/ReplaceAnnotation.java @@ -0,0 +1,97 @@ +/* + * 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; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeUtils; + +@Value +@EqualsAndHashCode(callSuper = false) +public class ReplaceAnnotation extends Recipe { + @Option(displayName = "Annotation pattern to replace", + description = "An annotation pattern, expressed as a method pattern to replace.", + example = "@org.jetbrains.annotations.NotNull(\"Test\")") + String annotationPatternToReplace; + @Option(displayName = "Annotation template to insert", + description = "An annotation template to add instead of original one, will be parsed with `JavaTemplate`.", + example = "@NonNull") + String annotationTemplateToInsert; + @Option(displayName = "Type of inserted Annotation", + description = "The fully qualified class name of the annotation to insert.", + example = "lombok.NonNull") + String annotationFQN; + @Option(displayName = "Templates Artifact id", + description = "The Maven artifactId to load the inserted annotations type from, defaults to JDK internals.", + example = "lombok", + required = false) + String artifactId; + + @Override + public String getDisplayName() { + return "Replace Annotation"; + } + + @Override + public String getDescription() { + return "Replace an Annotation with another one if the annotation pattern matches. " + + "Only fixed parameters can be set in the replacement."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesType<>(annotationFQN, false), + new ReplaceAnnotationVisitor( + new AnnotationMatcher(annotationPatternToReplace), + JavaTemplate.builder(annotationTemplateToInsert) + .imports(annotationFQN) + .javaParser(JavaParser.fromJavaVersion().logCompilationWarningsAndErrors(true) + .classpath(artifactId)) + .build())); + } + + public static class ReplaceAnnotationVisitor extends JavaIsoVisitor { + private final AnnotationMatcher matcher; + private final JavaTemplate replacement; + + public ReplaceAnnotationVisitor(AnnotationMatcher annotationMatcher, JavaTemplate replacement) { + super(); + this.matcher = annotationMatcher; + this.replacement = replacement; + } + + @Override + public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) { + annotation = super.visitAnnotation(annotation, ctx); + + boolean replaceAnnotation = matcher.matches(annotation); + if (replaceAnnotation) { + JavaType replacedAnnotationType = annotation.getType(); + annotation = replacement.apply(getCursor(), annotation.getCoordinates().replace()); + JavaType insertedAnnotationType = annotation.getType(); + maybeRemoveImport(TypeUtils.asFullyQualified(replacedAnnotationType)); + maybeAddImport(TypeUtils.asFullyQualified(insertedAnnotationType).getFullyQualifiedName(), false); + } + + return annotation; + } + } +} + diff --git a/rewrite-java/src/test/java/org/openrewrite/java/ReplaceAnnotationTest.java b/rewrite-java/src/test/java/org/openrewrite/java/ReplaceAnnotationTest.java new file mode 100644 index 00000000000..7c019c2548f --- /dev/null +++ b/rewrite-java/src/test/java/org/openrewrite/java/ReplaceAnnotationTest.java @@ -0,0 +1,146 @@ +/* + * 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; + +import org.junit.jupiter.api.Nested; +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 ReplaceAnnotationTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.parser(JavaParser.fromJavaVersion() + .logCompilationWarningsAndErrors(false) + .classpath("lombok", "annotations")); + } + + @Nested + class OnMatch { + @Test + void matchNoPrams() { + rewriteRun( + spec -> spec.recipe(new ReplaceAnnotation("@org.jetbrains.annotations.NotNull", + "@NonNull", "lombok.NonNull", "lombok")), + java( + """ + import org.jetbrains.annotations.NotNull; + + class A { + @NotNull + String testMethod() {} + } + """, """ + import lombok.NonNull; + + class A { + @NonNull + String testMethod() {} + } + """ + )); + } + + @Test + @DocumentExample + void matchWithPrams() { + rewriteRun( + spec -> spec.recipe(new ReplaceAnnotation("@org.jetbrains.annotations.NotNull(\"Test\")", + "@NonNull", "lombok.NonNull", "lombok")), + java( + """ + import org.jetbrains.annotations.NotNull; + + class A { + @NotNull("Test") + String testMethod() {} + } + """, """ + import lombok.NonNull; + + class A { + @NonNull + String testMethod() {} + } + """ + )); + } + + @Test + void insertWithParams() { + rewriteRun( + spec -> spec.recipe(new ReplaceAnnotation("@lombok.NonNull", + "@NotNull(\"Test\")", "org.jetbrains.annotations.NotNull", "annotations")), + java( + """ + import lombok.NonNull; + + class A { + @NonNull + String testMethod() {} + } + """, """ + import org.jetbrains.annotations.NotNull; + + class A { + @NotNull("Test") + String testMethod() {} + } + """ + )); + } + } + + @Nested + class NoMatch { + @Test + void noMatchOtherType() { + rewriteRun( + spec -> spec.recipe(new ReplaceAnnotation("@org.jetbrains.annotations.NotNull", + "@NonNull", "lombok.NonNull", "lombok")), + java( + """ + import org.jetbrains.annotations.Nullable; + + class A { + @Nullable("Test") + String testMethod() {} + } + """ + )); + } + + @Test + void noMatchParameter() { + rewriteRun( + spec -> spec.recipe(new ReplaceAnnotation("@org.jetbrains.annotations.NotNull(\"Test\")", + "@NonNull", "lombok.NonNull", "lombok")), + java( + """ + import org.jetbrains.annotations.Nullable; + + class A { + @Nullable("Other") + String testMethod() {} + } + """ + )); + } + } +} From 5577a221d13633565518b6611c244a9f9fb57291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Merlin=20B=C3=B6gershausen?= Date: Fri, 31 May 2024 14:59:03 +0200 Subject: [PATCH 2/2] Apply suggestions from Knut regarding usage of ShortenFullyQualifiedTypeReferences and in consequence remove not needed parameters --- .../openrewrite/java/ReplaceAnnotation.java | 62 ++++--- .../java/ReplaceAnnotationTest.java | 153 ++++++++---------- 2 files changed, 93 insertions(+), 122 deletions(-) diff --git a/rewrite-java/src/main/java/org/openrewrite/java/ReplaceAnnotation.java b/rewrite-java/src/main/java/org/openrewrite/java/ReplaceAnnotation.java index ec731804b52..08ed3a30979 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/ReplaceAnnotation.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/ReplaceAnnotation.java @@ -17,32 +17,28 @@ import lombok.EqualsAndHashCode; import lombok.Value; -import org.openrewrite.*; -import org.openrewrite.java.search.UsesType; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Option; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaCoordinates; import org.openrewrite.java.tree.JavaType; import org.openrewrite.java.tree.TypeUtils; @Value @EqualsAndHashCode(callSuper = false) public class ReplaceAnnotation extends Recipe { - @Option(displayName = "Annotation pattern to replace", - description = "An annotation pattern, expressed as a method pattern to replace.", + + @Option(displayName = "Annotation to replace", + description = "An annotation matcher, expressed as a method pattern to replace.", example = "@org.jetbrains.annotations.NotNull(\"Test\")") String annotationPatternToReplace; + @Option(displayName = "Annotation template to insert", description = "An annotation template to add instead of original one, will be parsed with `JavaTemplate`.", - example = "@NonNull") + example = "@org.jetbrains.annotations.NotNull(\"Null not permitted\")") String annotationTemplateToInsert; - @Option(displayName = "Type of inserted Annotation", - description = "The fully qualified class name of the annotation to insert.", - example = "lombok.NonNull") - String annotationFQN; - @Option(displayName = "Templates Artifact id", - description = "The Maven artifactId to load the inserted annotations type from, defaults to JDK internals.", - example = "lombok", - required = false) - String artifactId; @Override public String getDisplayName() { @@ -57,14 +53,9 @@ public String getDescription() { @Override public TreeVisitor getVisitor() { - return Preconditions.check(new UsesType<>(annotationFQN, false), - new ReplaceAnnotationVisitor( - new AnnotationMatcher(annotationPatternToReplace), - JavaTemplate.builder(annotationTemplateToInsert) - .imports(annotationFQN) - .javaParser(JavaParser.fromJavaVersion().logCompilationWarningsAndErrors(true) - .classpath(artifactId)) - .build())); + return new ReplaceAnnotationVisitor( + new AnnotationMatcher(annotationPatternToReplace), + JavaTemplate.builder(annotationTemplateToInsert).javaParser(JavaParser.fromJavaVersion()).build()); } public static class ReplaceAnnotationVisitor extends JavaIsoVisitor { @@ -78,19 +69,24 @@ public ReplaceAnnotationVisitor(AnnotationMatcher annotationMatcher, JavaTemplat } @Override - public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) { - annotation = super.visitAnnotation(annotation, ctx); - - boolean replaceAnnotation = matcher.matches(annotation); - if (replaceAnnotation) { - JavaType replacedAnnotationType = annotation.getType(); - annotation = replacement.apply(getCursor(), annotation.getCoordinates().replace()); - JavaType insertedAnnotationType = annotation.getType(); - maybeRemoveImport(TypeUtils.asFullyQualified(replacedAnnotationType)); - maybeAddImport(TypeUtils.asFullyQualified(insertedAnnotationType).getFullyQualifiedName(), false); + public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext ctx) { + J.Annotation maybeReplacingAnnotation = super.visitAnnotation(a, ctx); + + boolean keepAnnotation = !matcher.matches(maybeReplacingAnnotation); + if (keepAnnotation) { + return maybeReplacingAnnotation; } - return annotation; + + JavaType.FullyQualified replacedAnnotationType = TypeUtils.asFullyQualified(maybeReplacingAnnotation.getType()); + maybeRemoveImport(replacedAnnotationType); + + JavaCoordinates replaceCoordinate = maybeReplacingAnnotation.getCoordinates().replace(); + J.Annotation replacement = this.replacement.apply(getCursor(), replaceCoordinate); + + doAfterVisit(ShortenFullyQualifiedTypeReferences.modifyOnly(replacement)); + + return replacement; } } } diff --git a/rewrite-java/src/test/java/org/openrewrite/java/ReplaceAnnotationTest.java b/rewrite-java/src/test/java/org/openrewrite/java/ReplaceAnnotationTest.java index 7c019c2548f..ad4dd3db6a9 100644 --- a/rewrite-java/src/test/java/org/openrewrite/java/ReplaceAnnotationTest.java +++ b/rewrite-java/src/test/java/org/openrewrite/java/ReplaceAnnotationTest.java @@ -15,6 +15,7 @@ */ package org.openrewrite.java; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; @@ -23,87 +24,71 @@ import static org.openrewrite.java.Assertions.java; +@Disabled class ReplaceAnnotationTest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { - spec.parser(JavaParser.fromJavaVersion() - .logCompilationWarningsAndErrors(false) - .classpath("lombok", "annotations")); + spec.parser(JavaParser.fromJavaVersion().classpath("lombok", "annotations")); } @Nested class OnMatch { @Test void matchNoPrams() { - rewriteRun( - spec -> spec.recipe(new ReplaceAnnotation("@org.jetbrains.annotations.NotNull", - "@NonNull", "lombok.NonNull", "lombok")), - java( - """ - import org.jetbrains.annotations.NotNull; - - class A { - @NotNull - String testMethod() {} - } - """, """ - import lombok.NonNull; - - class A { - @NonNull - String testMethod() {} - } - """ - )); + rewriteRun(spec -> spec.recipe(new ReplaceAnnotation("@org.jetbrains.annotations.NotNull", "@lombok.NonNull")), java(""" + import org.jetbrains.annotations.NotNull; + + class A { + @NotNull + String testMethod() {} + } + """, """ + import lombok.NonNull; + + class A { + @NonNull + String testMethod() {} + } + """)); } @Test @DocumentExample void matchWithPrams() { - rewriteRun( - spec -> spec.recipe(new ReplaceAnnotation("@org.jetbrains.annotations.NotNull(\"Test\")", - "@NonNull", "lombok.NonNull", "lombok")), - java( - """ - import org.jetbrains.annotations.NotNull; - - class A { - @NotNull("Test") - String testMethod() {} - } - """, """ - import lombok.NonNull; - - class A { - @NonNull - String testMethod() {} - } - """ - )); + rewriteRun(spec -> spec.recipe(new ReplaceAnnotation("@org.jetbrains.annotations.NotNull(\"Test\")", "@lombok.NonNull")), java(""" + import org.jetbrains.annotations.NotNull; + + class A { + @NotNull("Test") + String testMethod() {} + } + """, """ + import lombok.NonNull; + + class A { + @NonNull + String testMethod() {} + } + """)); } @Test void insertWithParams() { - rewriteRun( - spec -> spec.recipe(new ReplaceAnnotation("@lombok.NonNull", - "@NotNull(\"Test\")", "org.jetbrains.annotations.NotNull", "annotations")), - java( - """ - import lombok.NonNull; - - class A { - @NonNull - String testMethod() {} - } - """, """ - import org.jetbrains.annotations.NotNull; - - class A { - @NotNull("Test") - String testMethod() {} - } - """ - )); + rewriteRun(spec -> spec.recipe(new ReplaceAnnotation("@lombok.NonNull", "@org.jetbrains.annotations.NotNull(\"Test\")")), java(""" + import lombok.NonNull; + + class A { + @NonNull + String testMethod() {} + } + """, """ + import org.jetbrains.annotations.NotNull; + + class A { + @NotNull("Test") + String testMethod() {} + } + """)); } } @@ -111,36 +96,26 @@ String testMethod() {} class NoMatch { @Test void noMatchOtherType() { - rewriteRun( - spec -> spec.recipe(new ReplaceAnnotation("@org.jetbrains.annotations.NotNull", - "@NonNull", "lombok.NonNull", "lombok")), - java( - """ - import org.jetbrains.annotations.Nullable; - - class A { - @Nullable("Test") - String testMethod() {} - } - """ - )); + rewriteRun(spec -> spec.recipe(new ReplaceAnnotation("@org.jetbrains.annotations.NotNull", "@lombok.NonNull")), java(""" + import org.jetbrains.annotations.Nullable; + + class A { + @Nullable("Test") + String testMethod() {} + } + """)); } @Test void noMatchParameter() { - rewriteRun( - spec -> spec.recipe(new ReplaceAnnotation("@org.jetbrains.annotations.NotNull(\"Test\")", - "@NonNull", "lombok.NonNull", "lombok")), - java( - """ - import org.jetbrains.annotations.Nullable; - - class A { - @Nullable("Other") - String testMethod() {} - } - """ - )); + rewriteRun(spec -> spec.recipe(new ReplaceAnnotation("@org.jetbrains.annotations.NotNull(\"Test\")", "@lombok.NonNull")), java(""" + import org.jetbrains.annotations.Nullable; + + class A { + @Nullable("Other") + String testMethod() {} + } + """)); } } }