Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Fix PMD rule LiteralsFirstInComparisons for compareTo* and contentEquals #448

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions src/main/java/org/openrewrite/staticanalysis/EqualsAvoidsNull.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.openrewrite.staticanalysis;

import org.apache.commons.lang3.ObjectUtils;
import org.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.java.JavaIsoVisitor;
Expand Down Expand Up @@ -54,24 +55,23 @@ public Duration getEstimatedEffortPerOccurrence() {

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
JavaIsoVisitor<ExecutionContext> replacementVisitor = new JavaIsoVisitor<ExecutionContext>() {
@Override
public J visit(@Nullable Tree tree, ExecutionContext ctx) {
if (tree instanceof JavaSourceFile) {
JavaSourceFile cu = (JavaSourceFile) requireNonNull(tree);
EqualsAvoidsNullStyle style = cu.getStyle(EqualsAvoidsNullStyle.class);
if (style == null) {
style = Checkstyle.equalsAvoidsNull();
return Preconditions.check(
// new UsesMethod<>("java.lang.String *quals*(..)"),
new UsesMethod<>("java.lang.String *(..)"),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @timtebeek,
I need help finding a better regex for equals and compareTo. The current one is just a temporary fix—it might work, but it's inefficient.

new JavaIsoVisitor<ExecutionContext>() {
@Override
public J visit(@Nullable Tree tree, ExecutionContext ctx) {
if (tree instanceof JavaSourceFile) {
JavaSourceFile cu = (JavaSourceFile) requireNonNull(tree);
return new EqualsAvoidsNullVisitor<>(
ObjectUtils.defaultIfNull(cu.getStyle(EqualsAvoidsNullStyle.class),
Checkstyle.equalsAvoidsNull()))
.visitNonNull(cu, ctx);
}
//noinspection DataFlowIssue
return (J) tree;
}
return new EqualsAvoidsNullVisitor<>(style).visitNonNull(cu, ctx);
}
//noinspection DataFlowIssue
return (J) tree;
}
};
return Preconditions.check(
new UsesMethod<>("java.lang.String *quals*(..)"),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this by accident while wondering why the recipe wasn’t applied. I think it would be better to prioritize a functional approach—putting the most important things first. In a functional style, it’s easier to see and understand the flow right away.

Thanks for considering this!

replacementVisitor
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,18 @@ public class EqualsAvoidsNullVisitor<P> extends JavaVisitor<P> {
private static final String JAVA_LANG_STRING = "java.lang.String ";
private static final MethodMatcher EQUALS = new MethodMatcher(JAVA_LANG_STRING + "equals(java.lang.Object)");
private static final MethodMatcher EQUALS_IGNORE_CASE = new MethodMatcher(JAVA_LANG_STRING + "equalsIgnoreCase(java.lang.String)");
private static final MethodMatcher COMPARE_TO = new MethodMatcher(JAVA_LANG_STRING + "compareTo(java.lang.String)");
private static final MethodMatcher COMPARE_TO_IGNORE_CASE = new MethodMatcher(JAVA_LANG_STRING + "compareToIgnoreCase(java.lang.String)");
private static final MethodMatcher CONTENT_EQUALS = new MethodMatcher(JAVA_LANG_STRING + "contentEquals(java.lang.CharSequence)");

EqualsAvoidsNullStyle style;

@Override
public J visitMethodInvocation(J.MethodInvocation method, P p) {
J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, p);
boolean stringComparisonMethod = isStringComparisonMethod(m);
if (m.getSelect() != null && !(m.getSelect() instanceof J.Literal) &&
isStringComparisonMethod(m) && hasCompatibleArgument(m)) {
stringComparisonMethod && hasCompatibleArgument(m)) {

maybeHandleParentBinary(m);

Expand Down Expand Up @@ -90,7 +93,9 @@ private boolean hasCompatibleArgument(J.MethodInvocation m) {
private boolean isStringComparisonMethod(J.MethodInvocation methodInvocation) {
return EQUALS.matches(methodInvocation) ||
(!style.getIgnoreEqualsIgnoreCase() && EQUALS_IGNORE_CASE.matches(methodInvocation)) ||
CONTENT_EQUALS.matches(methodInvocation);
CONTENT_EQUALS.matches(methodInvocation) ||
COMPARE_TO.matches(methodInvocation) ||
COMPARE_TO_IGNORE_CASE.matches(methodInvocation);
}

private void maybeHandleParentBinary(J.MethodInvocation m) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,58 @@ public class A {
);
}

@DocumentExample
@Test
void invertConditional_compareTo() {
rewriteRun(
//language=java
java(
"""
public class A {
{
String s = null;
if(s.compareTo("test")) {}
}
}
""",
"""
public class A {
{
String s = null;
if("test".compareTo(s)) {}
}
}
"""
)
);
}

@DocumentExample
@Test
void invertConditional_compareToIgnoreCase() {
rewriteRun(
//language=java
java(
"""
public class A {
{
String s = null;
if(s.compareToIgnoreCase("test")) {}
}
}
""",
"""
public class A {
{
String s = null;
if("test".compareToIgnoreCase(s)) {}
}
}
"""
)
);
}

@Test
void removeUnnecessaryNullCheck() {
rewriteRun(
Expand Down Expand Up @@ -447,6 +499,15 @@ public class A {
System.out.println(s.compareToIgnoreCase("test"));
}
}
""",
"""
public class A {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure why we don’t have a second comparison method here—you guys probably know better. Looking at the code, it seems like the goal is to avoid an NPE.

This test was the only one failing, which caught my attention, so I decided to conclude it and ask the team for their thoughts.

Thanks!

{
String s = null;
System.out.println("test".compareTo(s));
System.out.println("test".compareToIgnoreCase(s));
}
}
"""
)
);
Expand Down
Loading