From e9e4c227de90aa725412eb3a4fde30e3d0b00788 Mon Sep 17 00:00:00 2001 From: arodionov Date: Fri, 18 Apr 2025 10:14:42 +0200 Subject: [PATCH 1/4] [draft] Remove Spring annotations if they repeating in subclasses --- ...otRepeatSpringAnnotationsInSubclasses.java | 53 +++++++++ ...peatSpringAnnotationsInSubclassesTest.java | 106 ++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 src/main/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclasses.java create mode 100644 src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java diff --git a/src/main/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclasses.java b/src/main/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclasses.java new file mode 100644 index 000000000..1cc140dce --- /dev/null +++ b/src/main/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclasses.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 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.spring; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.J; + +public class NotRepeatSpringAnnotationsInSubclasses extends Recipe { + + @Override + public String getDisplayName() { + return "Remove Spring annotations if they repeating in subclasses"; + } + + @Override + public String getDescription() { + return "Remove Spring annotations if they repeating in subclasses."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesType<>("org.springframework.web.bind.annotation.PostMapping", false), new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + return cd; + } + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext executionContext) { + return super.visitMethodDeclaration(method, executionContext); + } + }); + } +} diff --git a/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java b/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java new file mode 100644 index 000000000..951e11d9d --- /dev/null +++ b/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java @@ -0,0 +1,106 @@ +/* + * 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.spring; + +import org.junit.jupiter.api.Test; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class NotRepeatSpringAnnotationsInSubclassesTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new NotRepeatSpringAnnotationsInSubclasses()) + .parser(JavaParser.fromJavaVersion().classpath("spring-beans", "spring-boot", + "spring-context", "spring-core", "spring-web")); + } + + @Test + void removeLeadingAutowiredAnnotation() { + //language=java + rewriteRun( + java( + """ + import org.springframework.web.bind.annotation.PathVariable; + import org.springframework.web.bind.annotation.PostMapping; + import org.springframework.web.bind.annotation.RequestBody; + + public interface UserApi { + + @PostMapping("/users/{id}") + String updateUser( + @PathVariable("id") Long id, + @RequestBody UserData request + ); + + class UserData { + private String firstName; + private String lastName; + + public String getFirstName() {return firstName;} + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + } + } + """), + java( + """ + import org.springframework.web.bind.annotation.PathVariable; + import org.springframework.web.bind.annotation.PostMapping; + import org.springframework.web.bind.annotation.RequestBody; + import org.springframework.web.bind.annotation.RestController; + + @RestController + public class UserController implements UserApi { + + @PostMapping("/users/{id}") + String updateUser( + @PathVariable("id") Long id, + @RequestBody UserData request + ) { + return "User " + id + " updated: " + request.getFirstName() + " " + request.getLastName(); + } + } + """, + """ + import org.springframework.web.bind.annotation.RestController; + + @RestController + public class UserController implements UserApi { + + @Override + public String updateUser(Long id, UserData request) { + return "User " + id + " updated: " + request.getFirstName() + " " + request.getLastName(); + } + } + """ + ) + ); + } +} From 60339e02cd4f725ffad685c0ed5d1f45be45c8d3 Mon Sep 17 00:00:00 2001 From: Andrii Rodionov Date: Fri, 18 Apr 2025 10:26:56 +0200 Subject: [PATCH 2/4] Update src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- ...peatSpringAnnotationsInSubclassesTest.java | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java b/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java index 951e11d9d..73d5c9f0c 100644 --- a/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java +++ b/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java @@ -37,29 +37,30 @@ void removeLeadingAutowiredAnnotation() { rewriteRun( java( """ - import org.springframework.web.bind.annotation.PathVariable; - import org.springframework.web.bind.annotation.PostMapping; - import org.springframework.web.bind.annotation.RequestBody; - - public interface UserApi { - - @PostMapping("/users/{id}") - String updateUser( - @PathVariable("id") Long id, - @RequestBody UserData request - ); - - class UserData { - private String firstName; - private String lastName; - - public String getFirstName() {return firstName;} - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { + import org.springframework.web.bind.annotation.PathVariable; + import org.springframework.web.bind.annotation.PostMapping; + import org.springframework.web.bind.annotation.RequestBody; + public interface UserApi { + @PostMapping("/users/{id}") + String updateUser( + @PathVariable("id") Long id, + @RequestBody UserData request + ); + class UserData { + private String firstName; + private String lastName; + public String getFirstName() {return firstName;} + public void setFirstName(String firstName) { + this.firstName = firstName; + } + public String getLastName() { + return lastName; + } + public void setLastName(String lastName) { + this.lastName = lastName; + } + """ + ), return lastName; } From 2ea2faa59c396ea4a767b15823dbea7a41f451d7 Mon Sep 17 00:00:00 2001 From: Andrii Rodionov Date: Thu, 24 Apr 2025 09:51:42 +0200 Subject: [PATCH 3/4] - detect repeating and remove methods annotation --- ...otRepeatSpringAnnotationsInSubclasses.java | 42 +++++++++++++++-- ...peatSpringAnnotationsInSubclassesTest.java | 47 ++++++++++--------- 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclasses.java b/src/main/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclasses.java index 1cc140dce..49af129c0 100644 --- a/src/main/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclasses.java +++ b/src/main/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclasses.java @@ -19,9 +19,19 @@ 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.RemoveAnnotation; import org.openrewrite.java.search.UsesType; import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; public class NotRepeatSpringAnnotationsInSubclasses extends Recipe { @@ -37,7 +47,8 @@ public String getDescription() { @Override public TreeVisitor getVisitor() { - return Preconditions.check(new UsesType<>("org.springframework.web.bind.annotation.PostMapping", false), new JavaIsoVisitor() { + //return Preconditions.check(new UsesType<>("org.springframework.web.bind.annotation.PostMapping", false), new JavaIsoVisitor() { + return new JavaIsoVisitor() { @Override public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); @@ -45,9 +56,32 @@ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, Ex } @Override - public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext executionContext) { - return super.visitMethodDeclaration(method, executionContext); + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx); + + Optional overriddenMethod = TypeUtils.findOverriddenMethod(md.getMethodType()); + if (overriddenMethod.isPresent()) { + + JavaType.Method overrideMethod = overriddenMethod.get(); + + List baseAnnotations = overrideMethod.getAnnotations(); + List methodAnnotations = md.getMethodType().getAnnotations(); + List nonRepeated = methodAnnotations.stream() + .filter(a -> baseAnnotations.stream().noneMatch(b -> TypeUtils.isOfType(a, b))) + .collect(Collectors.toList()); + + List annotations = ListUtils.map(md.getLeadingAnnotations(), + a -> { + if (nonRepeated.stream().noneMatch(n -> TypeUtils.isOfType(a.getType(), ((JavaType.Annotation)n).getType()))) + return (J.Annotation) new RemoveAnnotation(a.getType().toString()).getVisitor().visit(a, ctx, getCursor().getParentOrThrow()); + return a; + }); + md = md.withLeadingAnnotations(annotations); + + + } + return md; } - }); + }; } } diff --git a/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java b/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java index 73d5c9f0c..19730a7fc 100644 --- a/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java +++ b/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java @@ -28,48 +28,49 @@ class NotRepeatSpringAnnotationsInSubclassesTest implements RewriteTest { public void defaults(RecipeSpec spec) { spec.recipe(new NotRepeatSpringAnnotationsInSubclasses()) .parser(JavaParser.fromJavaVersion().classpath("spring-beans", "spring-boot", - "spring-context", "spring-core", "spring-web")); - } - - @Test - void removeLeadingAutowiredAnnotation() { - //language=java - rewriteRun( - java( - """ - import org.springframework.web.bind.annotation.PathVariable; + "spring-context", "spring-core", "spring-web") + .dependsOn( + """ + import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; + public interface UserApi { @PostMapping("/users/{id}") String updateUser( @PathVariable("id") Long id, @RequestBody UserData request ); + class UserData { private String firstName; private String lastName; - public String getFirstName() {return firstName;} + + public String getFirstName() { + return firstName; + } + public void setFirstName(String firstName) { this.firstName = firstName; } + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; - } - """ - ), - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } } } - """), + } + """ + )); + } + + @Test + void removeLeadingAutowiredAnnotation() { + //language=java + rewriteRun( java( """ import org.springframework.web.bind.annotation.PathVariable; @@ -80,8 +81,8 @@ public void setLastName(String lastName) { @RestController public class UserController implements UserApi { - @PostMapping("/users/{id}") - String updateUser( + @Override @PostMapping("/users/{id}") + public String updateUser( @PathVariable("id") Long id, @RequestBody UserData request ) { From 198b052e31c3eeda9e195e2aecb23ee6ee1ab1d0 Mon Sep 17 00:00:00 2001 From: Andrii Rodionov Date: Thu, 24 Apr 2025 16:21:13 +0200 Subject: [PATCH 4/4] - add test only for annotated method --- ...otRepeatSpringAnnotationsInSubclasses.java | 22 ++--- ...peatSpringAnnotationsInSubclassesTest.java | 94 +++++++++++++------ 2 files changed, 75 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclasses.java b/src/main/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclasses.java index 49af129c0..b0c8663a4 100644 --- a/src/main/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclasses.java +++ b/src/main/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclasses.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * 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. @@ -16,19 +16,15 @@ package org.openrewrite.java.spring; 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.RemoveAnnotation; -import org.openrewrite.java.search.UsesType; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.JavaType; import org.openrewrite.java.tree.TypeUtils; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -42,7 +38,7 @@ public String getDisplayName() { @Override public String getDescription() { - return "Remove Spring annotations if they repeating in subclasses."; + return "Remove Spring annotations in subclasses if they present in base classes."; } @Override @@ -51,8 +47,7 @@ public TreeVisitor getVisitor() { return new JavaIsoVisitor() { @Override public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { - J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); - return cd; + return super.visitClassDeclaration(classDecl, ctx); } @Override @@ -66,19 +61,20 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex List baseAnnotations = overrideMethod.getAnnotations(); List methodAnnotations = md.getMethodType().getAnnotations(); - List nonRepeated = methodAnnotations.stream() - .filter(a -> baseAnnotations.stream().noneMatch(b -> TypeUtils.isOfType(a, b))) + List repeated = methodAnnotations.stream() + .filter(a -> baseAnnotations.stream().anyMatch(b -> TypeUtils.isOfType(a, b))) .collect(Collectors.toList()); List annotations = ListUtils.map(md.getLeadingAnnotations(), a -> { - if (nonRepeated.stream().noneMatch(n -> TypeUtils.isOfType(a.getType(), ((JavaType.Annotation)n).getType()))) - return (J.Annotation) new RemoveAnnotation(a.getType().toString()).getVisitor().visit(a, ctx, getCursor().getParentOrThrow()); + if (repeated.stream().anyMatch(n -> TypeUtils.isOfType(a.getType(), ((JavaType.Annotation) n).getType()))) { + return (J.Annotation) new RemoveAnnotation(a.getType().toString()).getVisitor().visit(a, ctx, getCursor().getParentOrThrow()); + } return a; }); md = md.withLeadingAnnotations(annotations); - + repeated.forEach(this::maybeRemoveImport); } return md; } diff --git a/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java b/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java index 19730a7fc..949fbf1a6 100644 --- a/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java +++ b/src/testWithSpringBoot_1_5/java/org/openrewrite/java/spring/NotRepeatSpringAnnotationsInSubclassesTest.java @@ -15,7 +15,9 @@ */ package org.openrewrite.java.spring; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; import org.openrewrite.java.JavaParser; import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; @@ -31,44 +33,80 @@ public void defaults(RecipeSpec spec) { "spring-context", "spring-core", "spring-web") .dependsOn( """ - import org.springframework.web.bind.annotation.PathVariable; - import org.springframework.web.bind.annotation.PostMapping; - import org.springframework.web.bind.annotation.RequestBody; - - public interface UserApi { - @PostMapping("/users/{id}") - String updateUser( - @PathVariable("id") Long id, - @RequestBody UserData request - ); - - class UserData { - private String firstName; - private String lastName; - - public String getFirstName() { - return firstName; - } + import org.springframework.web.bind.annotation.PathVariable; + import org.springframework.web.bind.annotation.PostMapping; + import org.springframework.web.bind.annotation.RequestBody; + + public interface UserApi { + @PostMapping("/users/{id}") + String updateUser( + @PathVariable("id") Long id, + @RequestBody UserData request + ); + + class UserData { + private String firstName; + private String lastName; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } - public void setFirstName(String firstName) { - this.firstName = firstName; + public void setLastName(String lastName) { + this.lastName = lastName; + } } + } + """ + )); + } - public String getLastName() { - return lastName; + @Test + @DocumentExample + void removeMethodAnnotation() { + //language=java + rewriteRun( + java( + """ + import org.springframework.web.bind.annotation.PostMapping; + import org.springframework.web.bind.annotation.RestController; + + @RestController + public class UserController implements UserApi { + + @Override @PostMapping("/users/{id}") + public String updateUser(Long id, UserData request) { + return "User " + id + " updated: " + request.getFirstName() + " " + request.getLastName(); } + } + """, + """ + import org.springframework.web.bind.annotation.RestController; - public void setLastName(String lastName) { - this.lastName = lastName; + @RestController + public class UserController implements UserApi { + + @Override + public String updateUser(Long id, UserData request) { + return "User " + id + " updated: " + request.getFirstName() + " " + request.getLastName(); } } - } - """ - )); + """ + ) + ); } @Test - void removeLeadingAutowiredAnnotation() { + @Disabled + void removeMethodArgumentsAnnotation() { //language=java rewriteRun( java(