From 43df9b230332fb87dbcda944d4876dfeef3ac0a6 Mon Sep 17 00:00:00 2001 From: kuchang Date: Mon, 18 Nov 2024 23:52:52 +0800 Subject: [PATCH] Add ChangeMethodParameter for modify parameters in method declaration Signed-off-by: kuchang --- .../java/ChangeMethodParameter.java | 264 +++++++++++ .../java/ChangeMethodParameterTest.java | 409 ++++++++++++++++++ 2 files changed, 673 insertions(+) create mode 100644 rewrite-java/src/main/java/org/openrewrite/java/ChangeMethodParameter.java create mode 100644 rewrite-java/src/test/java/org/openrewrite/java/ChangeMethodParameterTest.java diff --git a/rewrite-java/src/main/java/org/openrewrite/java/ChangeMethodParameter.java b/rewrite-java/src/main/java/org/openrewrite/java/ChangeMethodParameter.java new file mode 100644 index 00000000000..be2cc4da8a8 --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/ChangeMethodParameter.java @@ -0,0 +1,264 @@ +/* + * 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.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.java.search.DeclaresType; +import org.openrewrite.java.service.ImportService; +import org.openrewrite.java.tree.*; +import org.openrewrite.marker.Markers; +import org.openrewrite.marker.SearchResult; + +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; + +@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 name", + description = "The new name of the parameter that gets updated.", + example = "name") + String parameterName; + + @Option(displayName = "Parameter index", + description = "A zero-based index that indicates the position at which the parameter will be added.", + example = "0") + @Nullable + Integer parameterIndex; + + @Override + public String getInstanceNameSuffix() { + return String.format("`%s %s` in methods `%s`", parameterType, parameterName, methodPattern); + } + + @Override + public String getDisplayName() { + return "Replace a method parameter for a method declaration"; + } + + @Override + public String getDescription() { + return "Replace a method parameter for a method declaration."; + } + + @Override + public TreeVisitor getVisitor() { + int idx = methodPattern.indexOf('#'); + idx = idx == -1 ? methodPattern.indexOf(' ') : idx; + boolean typePattern = idx != -1 && methodPattern.lastIndexOf('*', idx) != -1; + return Preconditions.check(typePattern ? new DeclaresMatchingType(methodPattern.substring(0, idx)) : new DeclaresType<>(methodPattern.substring(0, idx)), new ChangeMethodArgumentVisitor(methodPattern)); + } + + private class ChangeMethodArgumentVisitor extends JavaIsoVisitor { + private final MethodMatcher methodMatcher; + + public ChangeMethodArgumentVisitor(String methodPattern) { + this.methodMatcher = new MethodMatcher(methodPattern); + } + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + method = super.visitMethodDeclaration(method, ctx); + if (parameterIndex == null || parameterIndex < 0) { + return method; + } + + J.ClassDeclaration enclosing = getCursor().firstEnclosing(J.ClassDeclaration.class); + if (enclosing != null && methodMatcher.matches(method, enclosing)) { + if (method.getParameters().isEmpty() || method.getParameters().size() <= parameterIndex) { + return method; + } + if (method.getParameters().get(parameterIndex) instanceof J.VariableDeclarations) { + J.VariableDeclarations parameter = (J.VariableDeclarations) method.getParameters().get(parameterIndex); + + TypeTree typeTree = createTypeTree(parameterType); + if (TypeUtils.isOfType(parameter.getType(), typeTree.getType())) { + return method; + } + + 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, + method.getMethodType(), + typeTree.getType(), + null + ) + ), + emptyList(), + null, + null + ) + )); + + method = autoFormat(changeParameter(method, parameter), parameter, ctx, getCursor().getParentTreeCursor()); + } + + } + return method; + } + + 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; + } + } + + private static class DeclaresMatchingType extends JavaIsoVisitor { + private final TypeMatcher typeMatcher; + + public DeclaresMatchingType(String type) { + this.typeMatcher = new TypeMatcher(type); + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + if (classDecl.getType() != null && typeMatcher.matches(classDecl.getType())) { + return SearchResult.found(classDecl); + } + return super.visitClassDeclaration(classDecl, ctx); + } + } +} diff --git a/rewrite-java/src/test/java/org/openrewrite/java/ChangeMethodParameterTest.java b/rewrite-java/src/test/java/org/openrewrite/java/ChangeMethodParameterTest.java new file mode 100644 index 00000000000..15a61354806 --- /dev/null +++ b/rewrite-java/src/test/java/org/openrewrite/java/ChangeMethodParameterTest.java @@ -0,0 +1,409 @@ +/* + * 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.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +public class ChangeMethodParameterTest implements RewriteTest { + + @DocumentExample + @Test + void primitive() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "long", "j", 0)), + java( + """ + package foo; + + public class Foo { + public void bar(int i) { + } + } + """, + """ + package foo; + + public class Foo { + public void bar(long j) { + } + } + """ + ) + ); + } + + @Test + void indexLargeThanZero() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "long", "k", 1)), + java( + """ + package foo; + + public class Foo { + public void bar(int i, int j) { + } + } + """, + """ + package foo; + + public class Foo { + public void bar(int i, long k) { + } + } + """ + ) + ); + } + + @Test + void sameName() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "long", "i", 0)), + java( + """ + package foo; + + public class Foo { + public void bar(int i) { + } + } + """, + """ + package foo; + + public class Foo { + public void bar(long i) { + } + } + """ + ) + ); + } + + @Test + void sameType() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "int", "j", 0)), + java( + """ + package foo; + + public class Foo { + public void bar(int i) { + } + } + """ + ) + ); + } + + @Test + void invalidIndex() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "int", "j", -1)), + java( + """ + package foo; + + public class Foo { + public void bar(int i) { + } + } + """ + ) + ); + } + + @Test + void notExistsIndex() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "int", "j", 1)), + java( + """ + package foo; + + public class Foo { + public void bar(int i) { + } + } + """ + ) + ); + } + + @Test + void typePattern() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("*..*#bar(..)", "long", "j", 0)), + java( + """ + package foo; + + public class Foo { + public void bar(int i) { + } + } + """, + """ + package foo; + + public class Foo { + public void bar(long j) { + } + } + """ + ) + ); + } + + @Test + void primitiveArray() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "int[]", "j", 0)), + java( + """ + package foo; + + public class Foo { + public void bar(int i) { + } + } + """, + """ + package foo; + + public class Foo { + public void bar(int[] j) { + } + } + """ + ) + ); + } + + @Test + void parameterized() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "java.util.List", "j", 0)), + java( + """ + package foo; + + public class Foo { + public void bar(int i) { + } + } + """, + """ + package foo; + + import java.util.List; + import java.util.regex.Pattern; + + public class Foo { + public void bar(List j) { + } + } + """ + ) + ); + } + + @Test + void wildcard() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "java.util.List", "j", 0)), + java( + """ + package foo; + + public class Foo { + public void bar(int i) { + } + } + """, + """ + package foo; + + import java.util.List; + + public class Foo { + public void bar(List j) { + } + } + """ + ) + ); + } + + @Test + void wildcardExtends() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "java.util.List", "j", 0)), + java( + """ + package foo; + + public class Foo { + public void bar(int i) { + } + } + """, + """ + package foo; + + import java.util.List; + + public class Foo { + public void bar(List j) { + } + } + """ + ) + ); + } + + @Test + void string() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "String", "i", 0)), + java( + """ + package foo; + + public class Foo { + public void bar(int j) { + } + + public void bar(int i) { + } + + public void bar(int j, int k) { + } + } + """, + """ + package foo; + + public class Foo { + public void bar(String i) { + } + + public void bar(String i) { + } + + public void bar(String i, int k) { + } + } + """ + ) + ); + } + + @Test + void first() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "long", "k", 0)), + java( + """ + package foo; + + public class Foo { + public void bar(int i) { + } + + public void bar(int i, int j) { + } + } + """, + """ + package foo; + + public class Foo { + public void bar(long k) { + } + + public void bar(long k, int j) { + } + } + """ + ) + ); + } + + @Test + void qualified() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "java.util.regex.Pattern", "p", 0)), + java( + """ + package foo; + + public class Foo { + public void bar(int i) { + } + public void bar(int i, int j) { + } + } + """, + """ + package foo; + + import java.util.regex.Pattern; + + public class Foo { + public void bar(Pattern p) { + } + + public void bar(Pattern p, int j) { + } + } + """ + ) + ); + } + + @Test + void object() { + rewriteRun( + spec -> spec.recipe(new ChangeMethodParameter("foo.Foo#bar(..)", "Object", "o", 0)), + java( + """ + package foo; + + public class Foo { + public void bar(int i) { + } + } + """, + """ + package foo; + + public class Foo { + public void bar(Object o) { + } + } + """ + ) + ); + } +}