diff --git a/gradle/licenseHeader.txt b/gradle/licenseHeader.txt index bcb1afc63e..701ddf254d 100644 --- a/gradle/licenseHeader.txt +++ b/gradle/licenseHeader.txt @@ -1,4 +1,4 @@ -Copyright 2021 the original author or authors. +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. diff --git a/src/main/java/org/openrewrite/staticanalysis/EqualsToContentEquals.java b/src/main/java/org/openrewrite/staticanalysis/EqualsToContentEquals.java new file mode 100644 index 0000000000..a261c13f1a --- /dev/null +++ b/src/main/java/org/openrewrite/staticanalysis/EqualsToContentEquals.java @@ -0,0 +1,76 @@ +/* + * 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.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.TypeUtils; + +import java.util.Collections; + +public class EqualsToContentEquals extends Recipe { + private static final TreeVisitor PRECONDITION = Preconditions.or( + new UsesType<>("java.lang.CharSequence", false), + new UsesType<>("java.lang.StringBuffer", false), + new UsesType<>("java.lang.StringBuilder", false) + ); + + @Override + public String getDisplayName() { + return "Use `String.contentEquals(CharSequence)` instead of `String.equals(CharSequence.toString())`"; + } + + @Override + public String getDescription() { + return "Use `String.contentEquals(CharSequence)` instead of `String.equals(CharSequence.toString())`."; + } + + public TreeVisitor getVisitor() { + return Preconditions.check(PRECONDITION, new EqualsToContentEqualsVisitor()); + } + + private static class EqualsToContentEqualsVisitor extends JavaIsoVisitor { + private static final MethodMatcher EQUALS_MATCHER = new MethodMatcher("String equals(Object)"); + private static final MethodMatcher TOSTRING_MATCHER = new MethodMatcher("java.lang.* toString()"); + + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation mi, ExecutionContext ctx) { + J.MethodInvocation m = super.visitMethodInvocation(mi, ctx); + if (!EQUALS_MATCHER.matches(m)) { + return m; + } + Expression equalsArgument = m.getArguments().get(0); + if (!TOSTRING_MATCHER.matches(equalsArgument)) { + return m; + } + J.MethodInvocation inv = (J.MethodInvocation) equalsArgument; + Expression toStringSelect = inv.getSelect(); + if (toStringSelect == null || !TypeUtils.isAssignableTo("java.lang.CharSequence", toStringSelect.getType())) { + return m; + } + // Strip out the toString() on the argument and replace with contentEquals + return m.withArguments(Collections.singletonList(toStringSelect)) + .withName(m.getName().withSimpleName("contentEquals")); + } + } +} diff --git a/src/test/java/org/openrewrite/staticanalysis/EqualsToContentEqualsTest.java b/src/test/java/org/openrewrite/staticanalysis/EqualsToContentEqualsTest.java new file mode 100644 index 0000000000..900203cc4c --- /dev/null +++ b/src/test/java/org/openrewrite/staticanalysis/EqualsToContentEqualsTest.java @@ -0,0 +1,153 @@ +/* + * 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.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class EqualsToContentEqualsTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec + .parser(JavaParser.fromJavaVersion()) + .recipe(new EqualsToContentEquals()); + } + + @Test + @DocumentExample + void replaceStringBuilder() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + boolean foo(StringBuilder sb) { + String str = "example string"; + return str.equals(sb.toString()); + } + } + """, + """ + class SomeClass { + boolean foo(StringBuilder sb) { + String str = "example string"; + return str.contentEquals(sb); + } + } + """ + ) + ); + } + + @Test + void onlyRunsOnCorrectInvocations() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + boolean foo(Integer number, String str) { + return str.equals(number.toString()); + } + } + """ + ) + ); + } + + @Test + void runsOnStringBuffer() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + boolean foo(StringBuffer sb, String str) { + return str.equals(sb.toString()); + } + } + """, + """ + class SomeClass { + boolean foo(StringBuffer sb, String str) { + return str.contentEquals(sb); + } + } + """ + ) + ); + } + + @Test + void runsOnCharSequence() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + boolean foo(CharSequence cs, String str) { + return str.equals(cs.toString()); + } + } + """, + """ + class SomeClass { + boolean foo(CharSequence cs, String str) { + return str.contentEquals(cs); + } + } + """ + ) + ); + } + + @Test + void runsOnNonStringVariablesAlso() { + //language=java + rewriteRun( + java( + """ + class SomeClass { + boolean foo(String str) { + return str.equals(getMessage().toString()); + } + + StringBuilder getMessage() { + return new StringBuilder("message"); + } + } + """, + """ + class SomeClass { + boolean foo(String str) { + return str.contentEquals(getMessage()); + } + + StringBuilder getMessage() { + return new StringBuilder("message"); + } + } + """ + ) + ); + } +}