diff --git a/src/main/java/org/openrewrite/java/spring/ChangeMethodParameter.java b/src/main/java/org/openrewrite/java/spring/ChangeMethodParameter.java new file mode 100644 index 000000000..b1cfdac56 --- /dev/null +++ b/src/main/java/org/openrewrite/java/spring/ChangeMethodParameter.java @@ -0,0 +1,244 @@ +/* + * 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.spring; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.DeclaresMethod; +import org.openrewrite.java.service.ImportService; +import org.openrewrite.java.tree.*; +import org.openrewrite.marker.Markers; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.openrewrite.Tree.randomId; + +/** + * A recipe to change parameter type for a method declaration. + *

+ * NOTE: This recipe is usually used for initial modification of parameter changes. + * After modifying method parameters using this recipe, you may also need to modify + * the method definition as needed to avoid compilation errors. + */ +@Value +@EqualsAndHashCode(callSuper = false) +public class ChangeMethodParameter extends Recipe { + + /** + * A method pattern that is used to find matching method declarations. + * See {@link MethodMatcher} for details on the expression's syntax. + */ + @Option(displayName = "Method pattern", + description = "A method pattern that is used to find the method declarations to modify.", + example = "com.yourorg.A foo(int, int)") + String methodPattern; + + @Option(displayName = "Parameter type", + description = "The new type of the parameter that gets updated.", + example = "java.lang.String") + String parameterType; + + @Option(displayName = "Parameter index", + description = "A zero-based index that indicates the position at which the parameter will be added.", + example = "0") + Integer parameterIndex; + + @Override + public String getInstanceNameSuffix() { + return String.format("`%s` in methods `%s`", parameterType, methodPattern); + } + + @Override + public String getDisplayName() { + return "Change parameter type for a method declaration"; + } + + @Override + public String getDescription() { + return "Change parameter type for a method declaration, identified by a method pattern."; + } + + @Override + public TreeVisitor getVisitor() { + MethodMatcher methodMatcher = new MethodMatcher(methodPattern, true); + return Preconditions.check(new DeclaresMethod<>(methodMatcher), new ChangeMethodArgumentVisitor(methodMatcher)); + } + + @RequiredArgsConstructor + private class ChangeMethodArgumentVisitor extends JavaIsoVisitor { + private final MethodMatcher methodMatcher; + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDeclaration, ExecutionContext ctx) { + J.MethodDeclaration md = super.visitMethodDeclaration(methodDeclaration, ctx); + + if (methodMatcher.matches(md.getMethodType())) { + if (md.getParameters().isEmpty() || + md.getParameters().get(0) instanceof J.Empty || + md.getParameters().size() <= parameterIndex) { + return md; + } + if (md.getParameters().get(parameterIndex) instanceof J.VariableDeclarations) { + J.VariableDeclarations parameter = (J.VariableDeclarations) md.getParameters().get(parameterIndex); + + TypeTree typeTree = createTypeTree(parameterType); + if (TypeUtils.isOfType(parameter.getType(), typeTree.getType())) { + return md; + } + + String parameterName = parameter.getVariables().get(0).getSimpleName(); + parameter = parameter.withTypeExpression(typeTree).withVariables(singletonList( + new J.VariableDeclarations.NamedVariable( + randomId(), + Space.EMPTY, + Markers.EMPTY, + new J.Identifier( + randomId(), + Space.EMPTY, + Markers.EMPTY, + emptyList(), + parameterName, + typeTree.getType(), + new JavaType.Variable( + null, + 0, + parameterName, + md.getMethodType(), + typeTree.getType(), + null + ) + ), + emptyList(), + null, + null + ) + )); + + md = autoFormat(changeParameter(md, parameter), parameter, ctx, getCursor().getParentTreeCursor()); + } + + } + return md; + } + + private J.MethodDeclaration changeParameter(J.MethodDeclaration method, J.VariableDeclarations parameter) { + List originalParameters = method.getParameters(); + List newParameters = new ArrayList<>(); + for (int i = 0; i < originalParameters.size(); i++) { + if (i == parameterIndex) { + newParameters.add(parameter); + } else { + newParameters.add(originalParameters.get(i)); + } + } + + method = method.withParameters(newParameters); + + if (parameter.getTypeExpression() != null && !(parameter.getTypeExpression() instanceof J.Identifier || parameter.getTypeExpression() instanceof J.Primitive)) { + doAfterVisit(service(ImportService.class).shortenFullyQualifiedTypeReferencesIn(parameter.getTypeExpression())); + } + return method; + } + + private TypeTree createTypeTree(String typeName) { + int arrayIndex = typeName.lastIndexOf('['); + if (arrayIndex != -1) { + TypeTree elementType = createTypeTree(typeName.substring(0, arrayIndex)); + return new J.ArrayType( + randomId(), + Space.EMPTY, + Markers.EMPTY, + elementType, + null, + JLeftPadded.build(Space.EMPTY), + new JavaType.Array(null, elementType.getType(), null) + ); + } + int genericsIndex = typeName.indexOf('<'); + if (genericsIndex != -1) { + TypeTree rawType = createTypeTree(typeName.substring(0, genericsIndex)); + List> typeParameters = new ArrayList<>(); + for (String typeParam : typeName.substring(genericsIndex + 1, typeName.lastIndexOf('>')).split(",")) { + typeParameters.add(JRightPadded.build((Expression) createTypeTree(typeParam.trim()))); + } + return new J.ParameterizedType( + randomId(), + Space.EMPTY, + Markers.EMPTY, + rawType, + JContainer.build(Space.EMPTY, typeParameters, Markers.EMPTY), + new JavaType.Parameterized(null, (JavaType.FullyQualified) rawType.getType(), null) + ); + } + JavaType.Primitive type = JavaType.Primitive.fromKeyword(typeName); + if (type != null) { + return new J.Primitive( + randomId(), + Space.EMPTY, + Markers.EMPTY, + type + ); + } + if (typeName.equals("?")) { + return new J.Wildcard( + randomId(), + Space.EMPTY, + Markers.EMPTY, + null, + null + ); + } + if (typeName.startsWith("?") && typeName.contains("extends")) { + return new J.Wildcard( + randomId(), + Space.EMPTY, + Markers.EMPTY, + new JLeftPadded<>(Space.SINGLE_SPACE, J.Wildcard.Bound.Extends, Markers.EMPTY), + createTypeTree(typeName.substring(typeName.indexOf("extends") + "extends".length() + 1).trim()).withPrefix(Space.SINGLE_SPACE) + ); + } + if (typeName.indexOf('.') == -1) { + String javaLangType = TypeUtils.findQualifiedJavaLangTypeName(typeName); + if (javaLangType != null) { + return new J.Identifier( + randomId(), + Space.EMPTY, + Markers.EMPTY, + emptyList(), + typeName, + JavaType.buildType(javaLangType), + null + ); + } + } + TypeTree typeTree = TypeTree.build(typeName); + // somehow the type attribution is incomplete, but `ChangeType` relies on this + if (typeTree instanceof J.FieldAccess) { + typeTree = ((J.FieldAccess) typeTree).withName(((J.FieldAccess) typeTree).getName().withType(typeTree.getType())); + } else if (typeTree.getType() == null) { + typeTree = ((J.Identifier) typeTree).withType(JavaType.ShallowClass.build(typeName)); + } + return typeTree; + } + } +} diff --git a/src/main/resources/META-INF/rewrite/spring-batch-5.0.yml b/src/main/resources/META-INF/rewrite/spring-batch-5.0.yml index cb885c4ea..bfb16c520 100644 --- a/src/main/resources/META-INF/rewrite/spring-batch-5.0.yml +++ b/src/main/resources/META-INF/rewrite/spring-batch-5.0.yml @@ -33,6 +33,7 @@ recipeList: - org.openrewrite.java.spring.batch.MigrateStepBuilderFactory - org.openrewrite.java.spring.batch.MigrateItemWriterWrite - org.openrewrite.java.spring.batch.RemoveDefaultBatchConfigurer + - org.openrewrite.java.spring.batch.UpgradeSkipPolicyParameterType - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: org.springframework.batch.core.metrics.BatchMetrics newFullyQualifiedTypeName: org.springframework.batch.core.observability.BatchMetrics @@ -63,3 +64,13 @@ recipeList: - org.openrewrite.java.spring.batch.ReplaceSupportClassWithItsInterface: fullyQualifiedClassName: org.springframework.batch.repeat.listener.RepeatListenerSupport fullyQualifiedInterfaceName: org.springframework.batch.repeat.RepeatListener +--- +type: specs.openrewrite.org/v1beta/recipe +name: org.openrewrite.java.spring.batch.UpgradeSkipPolicyParameterType +displayName: Change the type of `skipCount` parameter in `SkipPolicy` from `int` to `long` +description: The `skipCount` parameter in `org.springframework.batch.core.step.skip.SkipPolicy#shouldSkip` has been changed from `int` to `long`, this recipe updates the parameter type in the implementing classes. +recipeList: + - org.openrewrite.java.spring.ChangeMethodParameter: + methodPattern: org.springframework.batch.core.step.skip.SkipPolicy shouldSkip(java.lang.Throwable, int) + parameterIndex: 1 + parameterType: long diff --git a/src/test/java/org/openrewrite/java/spring/ChangeMethodParameterTest.java b/src/test/java/org/openrewrite/java/spring/ChangeMethodParameterTest.java new file mode 100644 index 000000000..c07226449 --- /dev/null +++ b/src/test/java/org/openrewrite/java/spring/ChangeMethodParameterTest.java @@ -0,0 +1,445 @@ +/* + * 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.spring; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class ChangeMethodParameterTest implements RewriteTest { + + @DocumentExample + @Test + void primitive() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "long", 0)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int i) { + } + } + """, + """ + package foo; + class Foo { + void bar(long i) { + } + } + """ + ) + ); + } + + @Test + void indexLargeThanZero() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "long", 1)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int i, int j) { + } + } + """, + """ + package foo; + class Foo { + void bar(int i, long j) { + } + } + """ + ) + ); + } + + @Test + void sameType() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "int", 0)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int i) { + } + } + """ + ) + ); + } + + @Test + void notExistsIndex() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "int", 1)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int i) { + } + } + """ + ) + ); + } + + @Test + void methodNotMatch() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "long", 0)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int i) { + } + } + """, + """ + package foo; + class Foo { + void bar(long i) { + } + } + """ + ) + ); + } + + @Test + void typePattern() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("*..*#bar(..)", "long", 0)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int i) { + } + } + """, + """ + package foo; + class Foo { + void bar(long i) { + } + } + """ + ) + ); + } + + @Test + void primitiveArray() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "int[]", 0)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int i) { + } + } + """, + """ + package foo; + class Foo { + void bar(int[] i) { + } + } + """ + ) + ); + } + + @Test + void parameterized() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "java.util.List", 0)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int i) { + } + } + """, + """ + package foo; + + import java.util.List; + import java.util.regex.Pattern; + + class Foo { + void bar(List i) { + } + } + """ + ) + ); + } + + @Test + void wildcard() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "java.util.List", 0)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int i) { + } + } + """, + """ + package foo; + + import java.util.List; + + class Foo { + void bar(List i) { + } + } + """ + ) + ); + } + + @Test + void wildcardExtends() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "java.util.List", 0)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int i) { + } + } + """, + """ + package foo; + + import java.util.List; + + class Foo { + void bar(List i) { + } + } + """ + ) + ); + } + + @Test + void string() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "String", 0)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int j) { + } + + void bar(int i) { + } + + void bar(int i, int j) { + } + } + """, + """ + package foo; + class Foo { + void bar(String j) { + } + + void bar(String i) { + } + + void bar(String i, int j) { + } + } + """ + ) + ); + } + + @Test + void first() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "long", 0)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int i) { + } + + void bar(int i, int j) { + } + } + """, + """ + package foo; + class Foo { + void bar(long i) { + } + + void bar(long i, int j) { + } + } + """ + ) + ); + } + + @Test + void qualified() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "java.util.regex.Pattern", 0)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int i) { + } + void bar(int i, int j) { + } + } + """, + """ + package foo; + + import java.util.regex.Pattern; + + class Foo { + void bar(Pattern i) { + } + + void bar(Pattern i, int j) { + } + } + """ + ) + ); + } + + @Test + void object() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "Object", 0)), + //language=java + java( + """ + package foo; + class Foo { + void bar(int i) { + } + } + """, + """ + package foo; + class Foo { + void bar(Object i) { + } + } + """ + ) + ); + } + + @Test + void fromInterface() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("b.B#m(String, String)", "long", 0)), + //language=java + java( + """ + package b; + public interface B { + boolean m(String i); + + boolean m(String i, String j); + } + """, + """ + package b; + public interface B { + boolean m(String i); + + boolean m(long i, String j); + } + """ + ), + //language=java + java( + """ + package foo; + import b.*; + + class Foo implements B { + @Override + public boolean m(String i) { + return true; + } + + @Override + public boolean m(String i, String j) { + return true; + } + } + """, + """ + package foo; + import b.*; + + class Foo implements B { + @Override + public boolean m(String i) { + return true; + } + + @Override + public boolean m(long i, String j) { + return true; + } + } + """ + ) + ); + } +} diff --git a/src/testWithSpringBoot_3_0/java/org/openrewrite/java/spring/batch/UpgradeSkipPolicyParameterTypeTest.java b/src/testWithSpringBoot_3_0/java/org/openrewrite/java/spring/batch/UpgradeSkipPolicyParameterTypeTest.java new file mode 100644 index 000000000..b3454d513 --- /dev/null +++ b/src/testWithSpringBoot_3_0/java/org/openrewrite/java/spring/batch/UpgradeSkipPolicyParameterTypeTest.java @@ -0,0 +1,74 @@ +/* + * 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.spring.batch; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class UpgradeSkipPolicyParameterTypeTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec + .recipeFromResource("/META-INF/rewrite/spring-batch-5.0.yml", "org.openrewrite.java.spring.batch.UpgradeSkipPolicyParameterType") + .parser(JavaParser.fromJavaVersion().classpathFromResources(new InMemoryExecutionContext(), + "spring-batch-core-4.3.+", + "spring-batch-infrastructure-4.3.+", + "spring-beans-4.3.30.RELEASE", + "spring-batch", + "spring-boot", + "spring-core", + "spring-context" + )); + } + + @DocumentExample + @Test + void replaceParameter() { + // language=java + rewriteRun( + java( + """ + import org.springframework.batch.core.step.skip.SkipLimitExceededException; + import org.springframework.batch.core.step.skip.SkipPolicy; + + public class MySkipPolicy implements SkipPolicy { + @Override + public boolean shouldSkip(Throwable throwable, int skipCount) { + return true; + } + } + """, + """ + import org.springframework.batch.core.step.skip.SkipLimitExceededException; + import org.springframework.batch.core.step.skip.SkipPolicy; + + public class MySkipPolicy implements SkipPolicy { + @Override + public boolean shouldSkip(Throwable throwable, long skipCount) { + return true; + } + } + """ + ) + ); + } +}