Skip to content

Commit 5a74d3b

Browse files
committed
Add recipes to replace Guava ImmutableSet.copyOf(), ImmutableList.copyOf(), and ImmutableMap.copyOf() with Java Set.copyOf(), List.copyOf(), and Map.copyOf()
Fixes #584 Implement recipes to replace Guava `Immutable{Set|List|Map}.copyOf()` calls with Java `{Set|List|Map}.copyOf()` calls. * Add `AbstractNoGuavaImmutableCopyOf` abstract class to handle the logic for replacing `copyOf()` calls. * Add `NoGuavaImmutableSetCopyOf`, `NoGuavaImmutableListCopyOf`, and `NoGuavaImmutableMapCopyOf` classes extending `AbstractNoGuavaImmutableCopyOf`. * Update `no-guava.yml` to include the new recipes. * Add test cases for `NoGuavaImmutableSetCopyOf`, `NoGuavaImmutableListCopyOf`, and `NoGuavaImmutableMapCopyOf`. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/openrewrite/rewrite-migrate-java/issues/584?shareId=XXXX-XXXX-XXXX-XXXX).
1 parent d67ebf8 commit 5a74d3b

File tree

8 files changed

+1752
-0
lines changed

8 files changed

+1752
-0
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
* <p>
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+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.migrate.guava;
17+
18+
import org.jspecify.annotations.Nullable;
19+
import org.openrewrite.ExecutionContext;
20+
import org.openrewrite.Preconditions;
21+
import org.openrewrite.Recipe;
22+
import org.openrewrite.TreeVisitor;
23+
import org.openrewrite.java.JavaTemplate;
24+
import org.openrewrite.java.JavaVisitor;
25+
import org.openrewrite.java.MethodMatcher;
26+
import org.openrewrite.java.search.UsesJavaVersion;
27+
import org.openrewrite.java.search.UsesType;
28+
import org.openrewrite.java.tree.*;
29+
30+
import java.time.Duration;
31+
import java.util.Objects;
32+
import java.util.stream.Collectors;
33+
34+
abstract class AbstractNoGuavaImmutableCopyOf extends Recipe {
35+
36+
private final String guavaType;
37+
private final String javaType;
38+
39+
AbstractNoGuavaImmutableCopyOf(String guavaType, String javaType) {
40+
this.guavaType = guavaType;
41+
this.javaType = javaType;
42+
}
43+
44+
private String getShortType(String fullyQualifiedType) {
45+
return fullyQualifiedType.substring(javaType.lastIndexOf(".") + 1);
46+
}
47+
48+
@Override
49+
public String getDisplayName() {
50+
return "Prefer `" + getShortType(javaType) + ".copyOf(..)` in Java 10 or higher";
51+
}
52+
53+
@Override
54+
public String getDescription() {
55+
return "Replaces `" + getShortType(guavaType) + ".copyOf(..)` if the returned type is immediately down-cast.";
56+
}
57+
58+
@Override
59+
public Duration getEstimatedEffortPerOccurrence() {
60+
return Duration.ofMinutes(10);
61+
}
62+
63+
@Override
64+
public TreeVisitor<?, ExecutionContext> getVisitor() {
65+
TreeVisitor<?, ExecutionContext> check = Preconditions.and(new UsesJavaVersion<>(10),
66+
new UsesType<>(guavaType, false));
67+
final MethodMatcher IMMUTABLE_MATCHER = new MethodMatcher(guavaType + " copyOf(..)");
68+
return Preconditions.check(check, new JavaVisitor<ExecutionContext>() {
69+
@Override
70+
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
71+
if (IMMUTABLE_MATCHER.matches(method) && isParentTypeDownCast(method)) {
72+
maybeRemoveImport(guavaType);
73+
maybeAddImport(javaType);
74+
75+
String template = method.getArguments().stream()
76+
.map(arg -> {
77+
if (arg.getType() instanceof JavaType.Primitive) {
78+
String type = "";
79+
if (JavaType.Primitive.Boolean == arg.getType()) {
80+
type = "Boolean";
81+
} else if (JavaType.Primitive.Byte == arg.getType()) {
82+
type = "Byte";
83+
} else if (JavaType.Primitive.Char == arg.getType()) {
84+
type = "Character";
85+
} else if (JavaType.Primitive.Double == arg.getType()) {
86+
type = "Double";
87+
} else if (JavaType.Primitive.Float == arg.getType()) {
88+
type = "Float";
89+
} else if (JavaType.Primitive.Int == arg.getType()) {
90+
type = "Integer";
91+
} else if (JavaType.Primitive.Long == arg.getType()) {
92+
type = "Long";
93+
} else if (JavaType.Primitive.Short == arg.getType()) {
94+
type = "Short";
95+
} else if (JavaType.Primitive.String == arg.getType()) {
96+
type = "String";
97+
}
98+
return TypeUtils.asFullyQualified(JavaType.buildType("java.lang." + type));
99+
} else {
100+
return TypeUtils.asFullyQualified(arg.getType());
101+
}
102+
})
103+
.filter(Objects::nonNull)
104+
.map(type -> "#{any(" + type.getFullyQualifiedName() + ")}")
105+
.collect(Collectors.joining(",", getShortType(javaType) + ".copyOf(", ")"));
106+
107+
return JavaTemplate.builder(template)
108+
.contextSensitive()
109+
.imports(javaType)
110+
.build()
111+
.apply(getCursor(),
112+
method.getCoordinates().replace(),
113+
method.getArguments().get(0) instanceof J.Empty ? new Object[]{} : method.getArguments().toArray());
114+
}
115+
return super.visitMethodInvocation(method, ctx);
116+
}
117+
118+
private boolean isParentTypeDownCast(MethodCall immutableMethod) {
119+
J parent = getCursor().dropParentUntil(J.class::isInstance).getValue();
120+
boolean isParentTypeDownCast = false;
121+
if (parent instanceof J.VariableDeclarations.NamedVariable) {
122+
isParentTypeDownCast = isParentTypeMatched(((J.VariableDeclarations.NamedVariable) parent).getType());
123+
} else if (parent instanceof J.Assignment) {
124+
J.Assignment a = (J.Assignment) parent;
125+
if (a.getVariable() instanceof J.Identifier && ((J.Identifier) a.getVariable()).getFieldType() != null) {
126+
isParentTypeDownCast = isParentTypeMatched(((J.Identifier) a.getVariable()).getFieldType().getType());
127+
} else if (a.getVariable() instanceof J.FieldAccess) {
128+
isParentTypeDownCast = isParentTypeMatched(a.getVariable().getType());
129+
}
130+
} else if (parent instanceof J.Return) {
131+
// Does not currently support returns in lambda expressions.
132+
J j = getCursor().dropParentUntil(is -> is instanceof J.MethodDeclaration || is instanceof J.CompilationUnit).getValue();
133+
if (j instanceof J.MethodDeclaration) {
134+
TypeTree returnType = ((J.MethodDeclaration) j).getReturnTypeExpression();
135+
if (returnType != null) {
136+
isParentTypeDownCast = isParentTypeMatched(returnType.getType());
137+
}
138+
}
139+
} else if (parent instanceof J.MethodInvocation) {
140+
J.MethodInvocation m = (J.MethodInvocation) parent;
141+
int index = m.getArguments().indexOf(immutableMethod);
142+
if (m.getMethodType() != null && index != -1) {
143+
isParentTypeDownCast = isParentTypeMatched(m.getMethodType().getParameterTypes().get(index));
144+
}
145+
} else if (parent instanceof J.NewClass) {
146+
J.NewClass c = (J.NewClass) parent;
147+
int index = 0;
148+
if (c.getConstructorType() != null) {
149+
for (Expression argument : c.getArguments()) {
150+
if (IMMUTABLE_MATCHER.matches(argument)) {
151+
break;
152+
}
153+
index++;
154+
}
155+
if (c.getConstructorType() != null) {
156+
isParentTypeDownCast = isParentTypeMatched(c.getConstructorType().getParameterTypes().get(index));
157+
}
158+
}
159+
} else if (parent instanceof J.NewArray) {
160+
J.NewArray a = (J.NewArray) parent;
161+
JavaType arrayType = a.getType();
162+
while (arrayType instanceof JavaType.Array) {
163+
arrayType = ((JavaType.Array) arrayType).getElemType();
164+
}
165+
166+
isParentTypeDownCast = isParentTypeMatched(arrayType);
167+
}
168+
return isParentTypeDownCast;
169+
}
170+
171+
private boolean isParentTypeMatched(@Nullable JavaType type) {
172+
JavaType.FullyQualified fq = TypeUtils.asFullyQualified(type);
173+
return TypeUtils.isOfClassType(fq, javaType) ||
174+
TypeUtils.isOfClassType(fq, "java.lang.Object");
175+
}
176+
});
177+
}
178+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
* <p>
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+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.migrate.guava;
17+
18+
public class NoGuavaImmutableListCopyOf extends AbstractNoGuavaImmutableCopyOf {
19+
public NoGuavaImmutableListCopyOf() {
20+
super("com.google.common.collect.ImmutableList", "java.util.List");
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
* <p>
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+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.migrate.guava;
17+
18+
public class NoGuavaImmutableMapCopyOf extends AbstractNoGuavaImmutableCopyOf {
19+
public NoGuavaImmutableMapCopyOf() {
20+
super("com.google.common.collect.ImmutableMap", "java.util.Map");
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
* <p>
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+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.migrate.guava;
17+
18+
public class NoGuavaImmutableSetCopyOf extends AbstractNoGuavaImmutableCopyOf {
19+
public NoGuavaImmutableSetCopyOf() {
20+
super("com.google.common.collect.ImmutableSet", "java.util.Set");
21+
}
22+
}

src/main/resources/META-INF/rewrite/no-guava.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ recipeList:
7777
- org.openrewrite.java.migrate.guava.NoGuavaImmutableListOf
7878
- org.openrewrite.java.migrate.guava.NoGuavaImmutableMapOf
7979
- org.openrewrite.java.migrate.guava.NoGuavaImmutableSetOf
80+
- org.openrewrite.java.migrate.guava.NoGuavaImmutableListCopyOf
81+
- org.openrewrite.java.migrate.guava.NoGuavaImmutableMapCopyOf
82+
- org.openrewrite.java.migrate.guava.NoGuavaImmutableSetCopyOf
8083
- org.openrewrite.java.migrate.guava.PreferJavaUtilObjectsRequireNonNullElse
8184
- org.openrewrite.java.dependencies.UpgradeDependencyVersion:
8285
groupId: io.springfox

0 commit comments

Comments
 (0)