Skip to content

Commit

Permalink
Rewrite Joiner.on(", ").join("a", "b") to `String.join(", ", "a", "…
Browse files Browse the repository at this point in the history
…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 #318

* Use `JavaTemplate` instead

---------

Co-authored-by: Knut Wannheden <[email protected]>
Co-authored-by: Tim te Beek <[email protected]>
  • Loading branch information
3 people authored Jan 11, 2024
1 parent f916eac commit a198cd6
Show file tree
Hide file tree
Showing 3 changed files with 361 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<String> getTags() {
return Collections.singleton("guava");
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(new UsesMethod<>(JOIN_METHOD_MATCHER), new PreferJavaStringJoinVisitor());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<ExecutionContext> {
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<Expression> 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<Expression> 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<Expression> 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<JavaType> 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<Expression> appendArguments(List<Expression> firstArgs, List<Expression> secondArgs) {
ArrayList<Expression> args = new ArrayList<>(firstArgs);
if (!secondArgs.isEmpty()) {
Expression e = secondArgs.remove(0);
args.add(e.withPrefix(e.getPrefix().withWhitespace(" ")));
args.addAll(secondArgs);
}
return args;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<String>());
}
""",
"""
import java.util.HashSet;
class Test {
String s = String.join(", ", new HashSet<String>());
}
"""
)
);
}

@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");
}
"""
)
);
}

}

0 comments on commit a198cd6

Please sign in to comment.