Skip to content

Commit 9831bdf

Browse files
authored
Add ConstructorToFluent recipe (#5145)
* Add ConstructorToFluent recipe The ConstructorToFluent recipe takes a convenience constructor like new GetObjectRequest(myBucket, myKey) and transforms it to use the fluent setter style instead: new GetObjectRequest().withBucketName(myBucket).withKey(myKey) This allows us to take advantage of other recipes that for example switch from using the fluent setters to using the V2-style builders. * Update param type matching * Review comments
1 parent 3126f62 commit 9831bdf

File tree

3 files changed

+352
-0
lines changed

3 files changed

+352
-0
lines changed

migration-tool/pom.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@
6666
</exclusion>
6767
</exclusions>
6868
</dependency>
69+
<dependency>
70+
<groupId>com.amazonaws</groupId>
71+
<artifactId>aws-java-sdk-s3</artifactId>
72+
<scope>test</scope>
73+
<version>1.12.472</version>
74+
<exclusions>
75+
<exclusion>
76+
<groupId>com.fasterxml.jackson</groupId>
77+
<artifactId>jackson-core</artifactId>
78+
</exclusion>
79+
</exclusions>
80+
</dependency>
6981
<dependency>
7082
<groupId>org.openrewrite</groupId>
7183
<artifactId>rewrite-maven</artifactId>
@@ -89,6 +101,12 @@
89101
<version>${junit5.version}</version>
90102
<scope>test</scope>
91103
</dependency>
104+
<dependency>
105+
<groupId>org.assertj</groupId>
106+
<artifactId>assertj-core</artifactId>
107+
<version>${assertj.version}</version>
108+
<scope>test</scope>
109+
</dependency>
92110
<dependency>
93111
<groupId>software.amazon.awssdk</groupId>
94112
<artifactId>utils</artifactId>
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.migration.internal.recipe;
17+
18+
import com.fasterxml.jackson.annotation.JsonCreator;
19+
import com.fasterxml.jackson.annotation.JsonProperty;
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.stream.Collectors;
23+
import org.openrewrite.ExecutionContext;
24+
import org.openrewrite.Recipe;
25+
import org.openrewrite.Tree;
26+
import org.openrewrite.TreeVisitor;
27+
import org.openrewrite.java.JavaVisitor;
28+
import org.openrewrite.java.tree.Expression;
29+
import org.openrewrite.java.tree.J;
30+
import org.openrewrite.java.tree.JContainer;
31+
import org.openrewrite.java.tree.JRightPadded;
32+
import org.openrewrite.java.tree.JavaType;
33+
import org.openrewrite.java.tree.Space;
34+
import org.openrewrite.java.tree.TypeUtils;
35+
import org.openrewrite.marker.Markers;
36+
import software.amazon.awssdk.annotations.SdkInternalApi;
37+
38+
@SdkInternalApi
39+
public class ConstructorToFluent extends Recipe {
40+
private final String clzzFqcn;
41+
private final List<String> parameterTypes;
42+
private final List<String> fluentNames;
43+
44+
@JsonCreator
45+
public ConstructorToFluent(@JsonProperty("clzzFqcn") String clzzFqcn,
46+
@JsonProperty("parameterTypes") List<String> parameterTypes,
47+
@JsonProperty("fluentNames") List<String> fluentNames) {
48+
this.clzzFqcn = clzzFqcn;
49+
this.parameterTypes = parameterTypes;
50+
this.fluentNames = fluentNames;
51+
52+
if (fluentNames.size() != parameterTypes.size()) {
53+
throw new IllegalArgumentException("parameterTypes and fluentNames must be the same length.");
54+
}
55+
}
56+
57+
@Override
58+
public String getDisplayName() {
59+
return "Moves constructor arguments to fluent setters";
60+
}
61+
62+
@Override
63+
public String getDescription() {
64+
return "A recipe that takes constructor arguments and moves them to the specified fluent setters on the object.";
65+
}
66+
67+
@Override
68+
public TreeVisitor<?, ExecutionContext> getVisitor() {
69+
List<JavaType> paramJavaTypes = parameterTypes.stream()
70+
.map(JavaType::buildType)
71+
.collect(Collectors.toList());
72+
return new Visitor(clzzFqcn, paramJavaTypes, fluentNames);
73+
}
74+
75+
private static class Visitor extends JavaVisitor<ExecutionContext> {
76+
private final JavaType.FullyQualified clzz;
77+
private final List<JavaType> parameterTypes;
78+
private final List<String> fluentNames;
79+
80+
Visitor(String clzz, List<JavaType> parameterTypes, List<String> fluentNames) {
81+
this.clzz = TypeUtils.asFullyQualified(JavaType.buildType(clzz));
82+
this.parameterTypes = parameterTypes;
83+
this.fluentNames = fluentNames;
84+
}
85+
86+
@Override
87+
public J visitNewClass(J.NewClass newClass, ExecutionContext executionContext) {
88+
JavaType.Method ctorType = newClass.getMethodType();
89+
90+
if (ctorType == null) {
91+
return newClass;
92+
}
93+
94+
if (!clzz.isAssignableFrom(ctorType.getDeclaringType())) {
95+
return newClass;
96+
}
97+
98+
List<JavaType> paramTypes = ctorType.getParameterTypes();
99+
100+
if (paramTypes.size() != this.parameterTypes.size()) {
101+
return newClass;
102+
}
103+
104+
for (int i = 0; i < parameterTypes.size(); ++i) {
105+
JavaType expected = this.parameterTypes.get(i);
106+
if (!TypeUtils.isAssignableTo(expected, paramTypes.get(i))) {
107+
return newClass;
108+
}
109+
}
110+
111+
// Remove all the arguments from the constructor
112+
List<Expression> arguments = newClass.getArguments();
113+
114+
ctorType = ctorType.withParameterTypes(Collections.emptyList())
115+
.withParameterNames(Collections.emptyList());
116+
117+
newClass = newClass.withArguments(Collections.emptyList())
118+
.withMethodType(ctorType);
119+
120+
JavaType.FullyQualified declaringType = ctorType.getDeclaringType();
121+
Expression select = newClass;
122+
for (int i = 0; i < parameterTypes.size(); ++i) {
123+
JavaType paramType = parameterTypes.get(i);
124+
String name = fluentNames.get(i);
125+
// Note we don't preserve prefix so we don't end up with
126+
// extra spaces after the opening paren like 'withFoo( arg)'
127+
Expression argExpr = arguments.get(i).withPrefix(Space.EMPTY);
128+
129+
select = addWither(select, name, paramType, argExpr, declaringType);
130+
}
131+
132+
return select;
133+
}
134+
135+
private static J.MethodInvocation addWither(Expression select, String simpleName,
136+
JavaType parameterType,
137+
Expression paramExpr,
138+
JavaType.FullyQualified declaringType) {
139+
JavaType.Method methodType = new JavaType.Method(
140+
null,
141+
0L,
142+
declaringType,
143+
simpleName,
144+
declaringType,
145+
Collections.singletonList(simpleName),
146+
Collections.singletonList(parameterType),
147+
null,
148+
null
149+
);
150+
151+
J.Identifier witherId = new J.Identifier(
152+
Tree.randomId(),
153+
Space.EMPTY,
154+
Markers.EMPTY,
155+
Collections.emptyList(),
156+
simpleName,
157+
methodType,
158+
null
159+
);
160+
161+
return new J.MethodInvocation(
162+
Tree.randomId(),
163+
Space.EMPTY,
164+
Markers.EMPTY,
165+
JRightPadded.build(select),
166+
null,
167+
witherId,
168+
JContainer.build(Collections.singletonList(JRightPadded.build(paramExpr))),
169+
methodType
170+
);
171+
}
172+
}
173+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.migration.internal.recipe;
17+
18+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
19+
import static org.openrewrite.java.Assertions.java;
20+
21+
import java.util.Arrays;
22+
import java.util.Collections;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.condition.EnabledOnJre;
25+
import org.junit.jupiter.api.condition.JRE;
26+
import org.openrewrite.java.Java8Parser;
27+
import org.openrewrite.test.RecipeSpec;
28+
import org.openrewrite.test.RewriteTest;
29+
30+
/**
31+
* Recipe that remaps invocations of model constructors that take some members as constructor parameters, so that the
32+
* parameters are specified using the fluent setter for the member instead.
33+
*/
34+
public class ConstructorToFluentTest implements RewriteTest {
35+
@Override
36+
public void defaults(RecipeSpec spec) {
37+
spec.parser(Java8Parser.builder().classpath(
38+
"aws-java-sdk-s3",
39+
"aws-java-sdk-core",
40+
"s3",
41+
"sdk-core"));
42+
spec.recipe(new ConstructorToFluent("com.amazonaws.services.s3.model.GetObjectRequest",
43+
Arrays.asList("java.lang.String", "java.lang.String"),
44+
Arrays.asList("withBucketName", "withKey")));
45+
46+
}
47+
48+
@Test
49+
public void newRecipe_listsNotMatch_throws() {
50+
assertThatThrownBy(() -> new ConstructorToFluent("foo",
51+
Collections.emptyList(),
52+
Collections.singletonList("bar")))
53+
.isInstanceOf(IllegalArgumentException.class);
54+
55+
assertThatThrownBy(() -> new ConstructorToFluent("foo",
56+
Collections.singletonList("bar"),
57+
Collections.emptyList()))
58+
.isInstanceOf(IllegalArgumentException.class);
59+
}
60+
61+
62+
63+
@Test
64+
@EnabledOnJre({JRE.JAVA_8})
65+
public void getObjectRequest_matchingCtorNotFound_doesNotRewrite() {
66+
rewriteRun(
67+
java("import com.amazonaws.services.s3.AmazonS3;\n"
68+
+ "import com.amazonaws.services.s3.model.GetObjectRequest;\n"
69+
+ "import com.amazonaws.services.s3.model.S3Object;\n"
70+
+ "import com.amazonaws.services.s3.model.S3ObjectInputStream;\n"
71+
+ "\n"
72+
+ "public class S3Example {\n"
73+
+ " public static void main(String[] args) {\n"
74+
+ " AmazonS3 s3 = null;\n"
75+
+ "\n"
76+
+ " GetObjectRequest getObject = new GetObjectRequest(\"bucket\", \"key\", \"version\");\n"
77+
+ "\n"
78+
+ " S3Object object = s3.getObject(getObject);\n"
79+
+ "\n"
80+
+ " S3ObjectInputStream objectContent = object.getObjectContent();\n"
81+
+ " }\n"
82+
+ "}\n",
83+
sourceSpecs -> {}
84+
)
85+
);
86+
}
87+
88+
@Test
89+
@EnabledOnJre({JRE.JAVA_8})
90+
public void getObjectRequest_bucketKeyCtor_convertedToFluent() {
91+
rewriteRun(
92+
java(
93+
"import com.amazonaws.services.s3.AmazonS3;\n"
94+
+ "import com.amazonaws.services.s3.model.GetObjectRequest;\n"
95+
+ "import com.amazonaws.services.s3.model.S3Object;\n"
96+
+ "\n"
97+
+ "public class S3Example {\n"
98+
+ " public static void main(String[] args) {\n"
99+
+ " AmazonS3 s3 = null;\n"
100+
+ "\n"
101+
+ " GetObjectRequest getObject = new GetObjectRequest(\"bucket\", \"key\");\n"
102+
+ "\n"
103+
+ " S3Object object = s3.getObject(getObject);\n"
104+
+ " }\n"
105+
+ "}\n",
106+
"import com.amazonaws.services.s3.AmazonS3;\n"
107+
+ "import com.amazonaws.services.s3.model.GetObjectRequest;\n"
108+
+ "import com.amazonaws.services.s3.model.S3Object;\n"
109+
+ "\n"
110+
+ "public class S3Example {\n"
111+
+ " public static void main(String[] args) {\n"
112+
+ " AmazonS3 s3 = null;\n"
113+
+ "\n"
114+
+ " GetObjectRequest getObject = new GetObjectRequest().withBucketName(\"bucket\").withKey(\"key\");\n"
115+
+ "\n"
116+
+ " S3Object object = s3.getObject(getObject);\n"
117+
+ " }\n"
118+
+ "}"
119+
)
120+
);
121+
}
122+
123+
// RequesterPays is a boolean primitive type, test to ensure the type matching can handle this
124+
@Test
125+
@EnabledOnJre({JRE.JAVA_8})
126+
public void getObjectRequest_bucketKeyRequesterPays_convertedToFluent() {
127+
rewriteRun(
128+
spec -> spec.recipe(new ConstructorToFluent("com.amazonaws.services.s3.model.GetObjectRequest",
129+
Arrays.asList("java.lang.String", "java.lang.String", "boolean"),
130+
Arrays.asList("withBucketName", "withKey", "withRequesterPays"))),
131+
java(
132+
"import com.amazonaws.services.s3.AmazonS3;\n"
133+
+ "import com.amazonaws.services.s3.model.GetObjectRequest;\n"
134+
+ "import com.amazonaws.services.s3.model.S3Object;\n"
135+
+ "\n"
136+
+ "public class S3Example {\n"
137+
+ " public static void main(String[] args) {\n"
138+
+ " AmazonS3 s3 = null;\n"
139+
+ "\n"
140+
+ " GetObjectRequest getObject = new GetObjectRequest(\"bucket\", \"key\", false);\n"
141+
+ "\n"
142+
+ " S3Object object = s3.getObject(getObject);\n"
143+
+ " }\n"
144+
+ "}\n",
145+
"import com.amazonaws.services.s3.AmazonS3;\n"
146+
+ "import com.amazonaws.services.s3.model.GetObjectRequest;\n"
147+
+ "import com.amazonaws.services.s3.model.S3Object;\n"
148+
+ "\n"
149+
+ "public class S3Example {\n"
150+
+ " public static void main(String[] args) {\n"
151+
+ " AmazonS3 s3 = null;\n"
152+
+ "\n"
153+
+ " GetObjectRequest getObject = new GetObjectRequest().withBucketName(\"bucket\").withKey(\"key\").withRequesterPays(false);\n"
154+
+ "\n"
155+
+ " S3Object object = s3.getObject(getObject);\n"
156+
+ " }\n"
157+
+ "}"
158+
)
159+
);
160+
}
161+
}

0 commit comments

Comments
 (0)