From a198cd6eb6a5df89963d0c7281c132f2ebc5cfa9 Mon Sep 17 00:00:00 2001 From: Tobias Lidskog Date: Thu, 11 Jan 2024 20:42:38 +0100 Subject: [PATCH] Rewrite `Joiner.on(", ").join("a", "b")` to `String.join(", ", "a", "b")` (#389) * Rewrite `Joiner.on(", ").join("a", "b")` to `String.join(", ", "a", "b")` Adds a recipe to rewrite calls to Guava Joiner#join with String.join in compatible cases. A call can be re-written if the delimiter is a String, and the arguments are an array, a list or an iterable of items that can be cast to a CharSequence. Addresses https://github.com/openrewrite/rewrite-migrate-java/issues/318 * Use `JavaTemplate` instead --------- Co-authored-by: Knut Wannheden Co-authored-by: Tim te Beek --- .../migrate/guava/PreferJavaStringJoin.java | 50 +++++ .../guava/PreferJavaStringJoinVisitor.java | 107 +++++++++ .../guava/PreferJavaStringJoinTest.java | 204 ++++++++++++++++++ 3 files changed, 361 insertions(+) create mode 100644 src/main/java/org/openrewrite/java/migrate/guava/PreferJavaStringJoin.java create mode 100644 src/main/java/org/openrewrite/java/migrate/guava/PreferJavaStringJoinVisitor.java create mode 100644 src/test/java/org/openrewrite/java/migrate/guava/PreferJavaStringJoinTest.java diff --git a/src/main/java/org/openrewrite/java/migrate/guava/PreferJavaStringJoin.java b/src/main/java/org/openrewrite/java/migrate/guava/PreferJavaStringJoin.java new file mode 100644 index 0000000000..564e03b66a --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/guava/PreferJavaStringJoin.java @@ -0,0 +1,50 @@ +/* + * 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.migrate.guava; + +import java.util.Collections; +import java.util.Set; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesMethod; + +public class PreferJavaStringJoin extends Recipe { + + static final MethodMatcher JOIN_METHOD_MATCHER = new MethodMatcher("com.google.common.base.Joiner join(..)"); + + @Override + public String getDisplayName() { + return "Prefer `String#join()` over Guava `Joiner#join()`"; + } + + @Override + public String getDescription() { + return "Replaces supported calls to `com.google.common.base.Joiner#join()` with `java.lang.String#join()`."; + } + + @Override + public Set getTags() { + return Collections.singleton("guava"); + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesMethod<>(JOIN_METHOD_MATCHER), new PreferJavaStringJoinVisitor()); + } +} diff --git a/src/main/java/org/openrewrite/java/migrate/guava/PreferJavaStringJoinVisitor.java b/src/main/java/org/openrewrite/java/migrate/guava/PreferJavaStringJoinVisitor.java new file mode 100644 index 0000000000..422aa7d627 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/guava/PreferJavaStringJoinVisitor.java @@ -0,0 +1,107 @@ +/* + * 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.migrate.guava; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +import java.util.ArrayList; +import java.util.List; + +import static org.openrewrite.java.migrate.guava.PreferJavaStringJoin.JOIN_METHOD_MATCHER; +import static org.openrewrite.java.tree.TypeUtils.isAssignableTo; +import static org.openrewrite.java.tree.TypeUtils.isString; + +class PreferJavaStringJoinVisitor extends JavaIsoVisitor { + private static final MethodMatcher ON_METHOD_MATCHER = + new MethodMatcher("com.google.common.base.Joiner on(String)"); + + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); + + if (!JOIN_METHOD_MATCHER.matches(mi) || !(mi.getSelect() instanceof J.MethodInvocation) || !ON_METHOD_MATCHER.matches(mi.getSelect())) { + return mi; + } + + boolean rewriteToJavaString = false; + + List arguments = mi.getArguments(); + if (arguments.size() == 1) { + JavaType javaType = arguments.get(0).getType(); + + rewriteToJavaString = isCompatibleArray(javaType) || isCompatibleIterable(javaType); + } else if (arguments.size() >= 2) { + rewriteToJavaString = isCompatibleArguments(arguments); + } + + if (rewriteToJavaString) { + J.MethodInvocation select = (J.MethodInvocation) mi.getSelect(); + assert select != null; + List newArgs = appendArguments(select.getArguments(), mi.getArguments()); + + maybeRemoveImport("com.google.common.base.Joiner"); + + J.MethodInvocation x = JavaTemplate.apply( + "String.join(#{any(java.lang.CharSequence)}", + getCursor(), + mi.getCoordinates().replace(), + select.getArguments().get(0) + ); + return x.withArguments(newArgs); + } + return mi; + } + + private boolean isCompatibleArguments(List arguments) { + return arguments.stream().map(Expression::getType).allMatch(PreferJavaStringJoinVisitor::isCharSequence); + } + + private boolean isCompatibleArray(@Nullable JavaType javaType) { + if (javaType instanceof JavaType.Array) { + return isCharSequence(((JavaType.Array) javaType).getElemType()); + } + return false; + } + + private boolean isCompatibleIterable(@Nullable JavaType javaType) { + if (isAssignableTo(Iterable.class.getName(), javaType) && javaType instanceof JavaType.Parameterized) { + List typeParameters = ((JavaType.Parameterized) javaType).getTypeParameters(); + return typeParameters.size() == 1 && isCharSequence(typeParameters.get(0)); + } + return false; + } + + private static boolean isCharSequence(@Nullable JavaType javaType) { + return isString(javaType) || isAssignableTo(CharSequence.class.getName(), javaType); + } + + private List appendArguments(List firstArgs, List secondArgs) { + ArrayList args = new ArrayList<>(firstArgs); + if (!secondArgs.isEmpty()) { + Expression e = secondArgs.remove(0); + args.add(e.withPrefix(e.getPrefix().withWhitespace(" "))); + args.addAll(secondArgs); + } + return args; + } +} diff --git a/src/test/java/org/openrewrite/java/migrate/guava/PreferJavaStringJoinTest.java b/src/test/java/org/openrewrite/java/migrate/guava/PreferJavaStringJoinTest.java new file mode 100644 index 0000000000..76ab75915d --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/guava/PreferJavaStringJoinTest.java @@ -0,0 +1,204 @@ +/* + * 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.migrate.guava; + +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 PreferJavaStringJoinTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec + .recipe(new PreferJavaStringJoin()) + .parser(JavaParser.fromJavaVersion().classpath("guava")); + } + + @Test + void joinStrings() { + //language=java + rewriteRun( + java( + """ + import com.google.common.base.Joiner; + + class Test { + String s = Joiner.on(", ").join("a", "b"); + } + """, + """ + class Test { + String s = String.join(", ", "a", "b"); + } + """ + ) + ); + } + + @Test + void joinStringArray() { + //language=java + rewriteRun( + java( + """ + import com.google.common.base.Joiner; + + class Test { + String s = Joiner.on(", ").join(new String[] {"a"}); + } + """, + """ + class Test { + String s = String.join(", ", new String[] {"a"}); + } + """ + ) + ); + } + + @Test + void joinIterables() { + //language=java + rewriteRun( + java( + """ + import com.google.common.base.Joiner; + import java.util.Set; + + class Test { + String s = Joiner.on(", ").join(Set.of("a")); + } + """, + """ + import java.util.Set; + + class Test { + String s = String.join(", ", Set.of("a")); + } + """ + ) + ); + } + + @Test + void joinMixedCharSequences() { + //language=java + rewriteRun( + java( + """ + import com.google.common.base.Joiner; + + class Test { + String s = Joiner.on(", ").join("a", new StringBuilder("b")); + } + """, + """ + class Test { + String s = String.join(", ", "a", new StringBuilder("b")); + } + """ + ) + ); + } + + @Test + void joinEmptyIterables() { + //language=java + rewriteRun( + java( + """ + import com.google.common.base.Joiner; + import java.util.HashSet; + + class Test { + String s = Joiner.on(", ").join(new HashSet()); + } + """, + """ + import java.util.HashSet; + + class Test { + String s = String.join(", ", new HashSet()); + } + """ + ) + ); + } + + @Test + void joinMethodOnSeparateLine() { + //language=java + rewriteRun( + java( + """ + import com.google.common.base.Joiner; + + class Test { + String s = Joiner.on(", ") + .join("a", "b"); + } + """, + """ + class Test { + String s = String.join(", ", "a", "b"); + } + """ + ) + ); + } + + @Test + void dontEditJoinersNotSupportedByString() { + //language=java + rewriteRun( + java( + """ + import com.google.common.base.Joiner; + import java.util.Set; + + class Test { + String s1 = Joiner.on(", ").join("a", 1); + String s2 = Joiner.on(", ").skipNulls().join("a", "b"); + String s3 = Joiner.on(", ").useForNull("null").join("a", "b"); + String s4 = Joiner.on(", ").join(Set.of("a").iterator()); + //String s5 = Joiner.on(',').join("a"); + } + """ + ) + ); + } + + @Test + void dontEditJoinerInstanceCaller() { + //language=java + rewriteRun( + java( + """ + import com.google.common.base.Joiner; + + class Test { + Joiner j = Joiner.on(", "); + String s1 = j.join("a", "b"); + } + """ + ) + ); + } + +} \ No newline at end of file