diff --git a/src/main/java/org/openrewrite/staticanalysis/RemoveToStringCallsFromArrayInstances.java b/src/main/java/org/openrewrite/staticanalysis/RemoveToStringCallsFromArrayInstances.java new file mode 100644 index 0000000000..dd13461e69 --- /dev/null +++ b/src/main/java/org/openrewrite/staticanalysis/RemoveToStringCallsFromArrayInstances.java @@ -0,0 +1,137 @@ +/* + * Copyright 2023 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.staticanalysis; + +import org.openrewrite.*; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class RemoveToStringCallsFromArrayInstances extends Recipe { + private static final MethodMatcher VALUEOF_MATCHER = new MethodMatcher("java.lang.String valueOf(java.lang.Object)"); + private static final MethodMatcher OBJECTS_TOSTRING_MATCHER = new MethodMatcher("java.util.Objects toString(Object)"); + private static final MethodMatcher TOSTRING_MATCHER = new MethodMatcher("java.lang.Object toString()"); + + private static final List PATTERNS = Arrays.asList( + "java.io.PrintStream print*(Object)", + "java.lang.String format*(..)", + "java.lang.StringBuilder insert(int, Object)", + "java.lang.StringBuilder append(Object)", + "java.io.PrintStream format(String, Object[])", + "java.io.PrintWriter print*(..)", + "java.io.PrintWriter format(..)" + ); + private static final List METHOD_MATCHERS = PATTERNS.stream().map(MethodMatcher::new).collect(Collectors.toList()); + + @Override + public Set getTags() { + return Collections.singleton("RSPEC-2116"); + } + + @Override + public String getDisplayName() { + return "Remove `toString()` calls on arrays"; + } + + @Override + public String getDescription() { + return "The result from `toString()` calls on arrays is largely useless. The output does not actually reflect" + + " the contents of the array. `Arrays.toString(array)` give the contents of the array."; + } + + public TreeVisitor getVisitor() { + return new RemoveToStringFromArraysVisitor(); + } + + private static class RemoveToStringFromArraysVisitor extends JavaVisitor { + @Override + public J visitMethodInvocation(J.MethodInvocation mi, ExecutionContext ctx) { + if (TOSTRING_MATCHER.matches(mi)) { + Expression select = mi.getSelect(); + if (select == null) { + return mi; + } + + return buildReplacement(select, mi); + } else if (METHOD_MATCHERS.stream().anyMatch(matcher -> matcher.matches(mi))) { + // deals with edge cases where .toString() is called implicitly + List arguments = mi.getArguments(); + for (Expression arg : arguments) { + if (arg.getType() instanceof JavaType.Array) { + getCursor().putMessage("METHOD_KEY", mi); + break; + } + } + }else if (OBJECTS_TOSTRING_MATCHER.matches(mi) || VALUEOF_MATCHER.matches(mi)) { + // method is static + Expression select = mi.getArguments().get(0); + maybeRemoveImport("java.util.Objects"); + + return buildReplacement(select, mi); + } + + return super.visitMethodInvocation(mi, ctx); + } + + public J buildReplacement(Expression select, J.MethodInvocation mi) { + if (!(select.getType() instanceof JavaType.Array)) { + return mi; + } + + maybeAddImport("java.util.Arrays"); + return JavaTemplate.builder("Arrays.toString(#{anyArray(java.lang.Object)})") + .imports("java.util.Arrays") + .build() + .apply(getCursor(), mi.getCoordinates().replace(), select); + } + + @Override + public Expression visitExpression(Expression exp, ExecutionContext ctx) { + Expression e = (Expression) super.visitExpression(exp, ctx); + if (e instanceof TypedTree && e.getType() instanceof JavaType.Array) { + Cursor c = getCursor().dropParentWhile(is -> is instanceof J.Parentheses || !(is instanceof Tree)); + if (c.getMessage("METHOD_KEY") != null || c.getMessage("BINARY_FOUND") != null) { + maybeAddImport("java.util.Arrays"); + return JavaTemplate.builder("Arrays.toString(#{anyArray(java.lang.Object)})") + .imports("java.util.Arrays") + .build() + .apply(getCursor(), e.getCoordinates().replace(), e); + } + } + + return e; + } + + @Override + public J.Binary visitBinary(J.Binary binary, ExecutionContext ctx) { + Expression left = binary.getLeft(); + Expression right = binary.getRight(); + + if (binary.getOperator() == J.Binary.Type.Addition && (left.getType() instanceof JavaType.Array || right.getType() instanceof JavaType.Array)) { + getCursor().putMessage("BINARY_FOUND", binary); + } + + return (J.Binary) super.visitBinary(binary, ctx); + } + } +} diff --git a/src/test/java/org/openrewrite/staticanalysis/EqualsToContentEqualsTest.java b/src/test/java/org/openrewrite/staticanalysis/EqualsToContentEqualsTest.java index 900203cc4c..118316316d 100644 --- a/src/test/java/org/openrewrite/staticanalysis/EqualsToContentEqualsTest.java +++ b/src/test/java/org/openrewrite/staticanalysis/EqualsToContentEqualsTest.java @@ -24,7 +24,6 @@ import static org.openrewrite.java.Assertions.java; class EqualsToContentEqualsTest implements RewriteTest { - @Override public void defaults(RecipeSpec spec) { spec diff --git a/src/test/java/org/openrewrite/staticanalysis/RemoveToStringCallsFromArrayInstancesTest.java b/src/test/java/org/openrewrite/staticanalysis/RemoveToStringCallsFromArrayInstancesTest.java new file mode 100644 index 0000000000..4b246d548d --- /dev/null +++ b/src/test/java/org/openrewrite/staticanalysis/RemoveToStringCallsFromArrayInstancesTest.java @@ -0,0 +1,503 @@ +/* + * Copyright 2023 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.staticanalysis; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.Issue; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +@SuppressWarnings({"ImplicitArrayToString", "UnnecessaryLocalVariable", "RedundantStringFormatCall", "MalformedFormatString", "PrimitiveArrayArgumentToVarargsMethod"}) +public class RemoveToStringCallsFromArrayInstancesTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec + .recipe(new RemoveToStringCallsFromArrayInstances()); + } + + @Test + @DocumentExample + @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/44") + void fixNonCompliantToString() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + public static void main(String[] args) { + String argStr = args.toString(); + } + } + """, + """ + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + String argStr = Arrays.toString(args); + } + } + """ + ) + ); + } + + @Test + void doesNotRunOnNonArrayInstances() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + public static void main(String[] args) { + int number = 5; + System.out.println(number.toString()); + } + } + """ + ) + ); + } + + @Test + void runsOnNonStringArrays() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + public static void main(String[] args) { + String arrStr = getNumArr().toString(); + } + + public int[] getNumArr() { + return new int[]{1, 2, 3, 4}; + } + } + """, + """ + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + String arrStr = Arrays.toString(getNumArr()); + } + + public int[] getNumArr() { + return new int[]{1, 2, 3, 4}; + } + } + """ + ) + ); + } + + @Test + void selectIsAMethod() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + public static void main(String[] args) { + String arrStr = getArr().toString(); + } + + public String[] getArr() { + String[] arr = {"test", "array"}; + return arr; + } + } + """, + """ + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + String arrStr = Arrays.toString(getArr()); + } + + public String[] getArr() { + String[] arr = {"test", "array"}; + return arr; + } + } + """ + ) + ); + } + + @Test + void printlnEdgeCase() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + public static void main(String[] args) { + int[] s = new int[]{1,2,3}; + System.out.println(s); + } + } + """, + """ + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + int[] s = new int[]{1,2,3}; + System.out.println(Arrays.toString(s)); + } + } + """ + ) + ); + } + + @Test + void printStringConcatenationTest() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + public static void main(String[] args) { + String[] arr = new String[]{"string ", "array"}; + System.out.print("Array: " + arr); + } + } + """, + """ + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + String[] arr = new String[]{"string ", "array"}; + System.out.print("Array: " + Arrays.toString(arr)); + } + } + """ + ) + ); + } + + @Test + void doesNotRunOnNormalStringConcat() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + public static void main(Stringp[] args) { + String strOne = "hello, "; + String strTwo = "world!"; + System.out.print(strOne + strTwo); + } + } + """ + ) + ); + } + + @Test + void stringFormatEdgeCase() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + public static void main(String[] args) { + int[] s = new int[]{1, 2, 3}; + System.out.println(String.format("s=%s", s)); + } + } + """, + """ + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + int[] s = new int[]{1, 2, 3}; + System.out.println(String.format("s=%s", Arrays.toString(s))); + } + } + """ + ) + ); + } + + @Test + void stringFormatMultipleArraysPassedIn() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + public static void main(String[] args) { + int[] s1 = new int[]{1, 2, 3}; + int[] s2 = new int[]{4, 5, 6}; + + System.out.println(String.format("s1=%s, s2=%s", s1, s2)); + } + } + """, + """ + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + int[] s1 = new int[]{1, 2, 3}; + int[] s2 = new int[]{4, 5, 6}; + + System.out.println(String.format("s1=%s, s2=%s", Arrays.toString(s1), Arrays.toString(s2))); + } + } + """ + ) + ); + } + + @Test + void stringFormatMultipleValuesWithArraysPassedIn() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + public static void main(String[] args) { + int[] s1 = new int[]{1, 2, 3}; + int[] s2 = new int[]{4, 5, 6}; + String name = "First array:"; + String secondName = "Second array:"; + + System.out.println(String.format("%s %s, %s %s", name, s1, secondName, s2)); + } + } + """, + """ + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + int[] s1 = new int[]{1, 2, 3}; + int[] s2 = new int[]{4, 5, 6}; + String name = "First array:"; + String secondName = "Second array:"; + + System.out.println(String.format("%s %s, %s %s", name, Arrays.toString(s1), secondName, Arrays.toString(s2))); + } + } + """ + ) + ); + } + + @Test + void worksWithObjectsToString() { + //language=java + rewriteRun( + java( + """ + import java.util.Objects; + + class SomeClass { + public static void main(String[] args) { + int[] arr = new int[]{1, 2, 3}; + String str_rep = Objects.toString(arr); + } + } + """, + """ + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + int[] arr = new int[]{1, 2, 3}; + String str_rep = Arrays.toString(arr); + } + } + """ + ) + ); + } + + @Test + void worksWithValueOf() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + public static void main(String[] args) { + String[] strings = new String[]{"bar"}; + + String str_rep = String.valueOf(strings); + } + } + """, + """ + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + String[] strings = new String[]{"bar"}; + + String str_rep = Arrays.toString(strings); + } + } + """ + ) + ); + } + + @Test + void worksWithInsert() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + public static void main(String[] args) { + StringBuilder builder = new StringBuilder("builder"); + String[] strings = new String[]{"string", "array"}; + + builder.insert(0, strings); + } + } + """, + """ + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + StringBuilder builder = new StringBuilder("builder"); + String[] strings = new String[]{"string", "array"}; + + builder.insert(0, Arrays.toString(strings)); + } + } + """ + ) + ); + } + + @Test + void worksWithAppend() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + public static void main(String[] args) { + StringBuilder builder = new StringBuilder("builder"); + String[] strings = new String[]{"array"}; + + builder.append(strings); + } + } + """, + """ + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + StringBuilder builder = new StringBuilder("builder"); + String[] strings = new String[]{"array"}; + + builder.append(Arrays.toString(strings)); + } + } + """ + ) + ); + } + + @Test + void worksWithPrintStreamFormat() { + //language=java + rewriteRun( + java( + """ + import java.io.PrintStream; + + class SomeClass { + public static void main(String[] args) { + PrintStream ps = new PrintStream(System.out); + String[] arr = new String[]{"test", "array"}; + + ps.format("formatting array: %s", arr); + ps.flush(); + } + } + """, + """ + import java.io.PrintStream; + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + PrintStream ps = new PrintStream(System.out); + String[] arr = new String[]{"test", "array"}; + + ps.format("formatting array: %s", Arrays.toString(arr)); + ps.flush(); + } + } + """ + ) + ); + } + + @Test + void printStreamPrintWorks() { + //language=java + rewriteRun( + java( + """ + import java.io.PrintStream; + + class SomeClass { + public static void main(String[] args) { + PrintStream ps = new PrintStream(System.out); + String[] arr = new String[]{"test", "array"}; + + ps.print(arr); + ps.flush(); + } + } + """, + """ + import java.io.PrintStream; + import java.util.Arrays; + + class SomeClass { + public static void main(String[] args) { + PrintStream ps = new PrintStream(System.out); + String[] arr = new String[]{"test", "array"}; + + ps.print(Arrays.toString(arr)); + ps.flush(); + } + } + """ + ) + ); + } + +}