Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
78eb2e7
wip
pinguin3245678 Oct 14, 2024
db907e9
wip
pinguin3245678 Oct 14, 2024
44432a0
wip BAR_CONTENT_EQUALS
pinguin3245678 Oct 14, 2024
6bbb603
add compareToIgnoreCase
pinguin3245678 Oct 14, 2024
3b95e60
wip
pinguin3245678 Oct 14, 2024
b6f48c8
undo
pinguin3245678 Oct 14, 2024
b187593
fix name for direct matching
pinguin3245678 Oct 14, 2024
735ca39
EQUALS_IGNORE_CASE
pinguin3245678 Oct 14, 2024
111baaa
add CONTENT_EQUALS
pinguin3245678 Oct 14, 2024
709a488
add compareToInverted
pinguin3245678 Oct 14, 2024
983b7f5
tmp fix
pinguin3245678 Oct 14, 2024
d9f83f6
wip
pinguin3245678 Oct 14, 2024
5b30904
fix working
pinguin3245678 Oct 14, 2024
13d7057
wiß
pinguin3245678 Oct 14, 2024
cc2c022
wip
pinguin3245678 Oct 14, 2024
e94ff44
wip
pinguin3245678 Oct 14, 2024
8addad3
wip
pinguin3245678 Oct 14, 2024
21f37a4
wip
pinguin3245678 Oct 14, 2024
e424b6f
finalize
pinguin3245678 Oct 14, 2024
9470cd4
sort
pinguin3245678 Oct 14, 2024
35ed8cf
add doc
pinguin3245678 Oct 14, 2024
a519472
undo
pinguin3245678 Oct 14, 2024
2f0e9b8
Update src/test/java/org/openrewrite/staticanalysis/EqualsAvoidsNullV…
Oct 14, 2024
931755a
Merge branch 'main' into EqualsAvoidsNullVisitor-extend-literalsfirst…
Oct 14, 2024
2c2b33a
fix Cast after
pinguin3245678 Oct 14, 2024
7e813dc
Merge remote-tracking branch 'origin/EqualsAvoidsNullVisitor-extend-l…
pinguin3245678 Oct 14, 2024
241262f
Update src/test/java/org/openrewrite/staticanalysis/EqualsAvoidsNullV…
Oct 14, 2024
4d78d78
Revert "fix Cast after"
pinguin3245678 Oct 14, 2024
ca9d3d7
wip: error: pattern matching in instanceof is not supported in -source 8
pinguin3245678 Oct 14, 2024
55b8299
cast
pinguin3245678 Oct 14, 2024
bec2a8e
Revert build.gradle changes; rewrite-bom manages rewrite-java
timtebeek Oct 14, 2024
477820a
Merge remote-tracking branch 'origin/EqualsAvoidsNullVisitor-extend-l…
pinguin3245678 Oct 14, 2024
e1cdc55
format
pinguin3245678 Oct 14, 2024
0f85697
condense
pinguin3245678 Oct 14, 2024
f2b94d4
condense
pinguin3245678 Oct 14, 2024
396d43b
apply style
pinguin3245678 Oct 14, 2024
6c205af
wip
pinguin3245678 Oct 14, 2024
586d85e
format fin
pinguin3245678 Oct 14, 2024
d1b6c1d
fix
pinguin3245678 Oct 14, 2024
0715f12
isStringComparisonMethod
pinguin3245678 Oct 14, 2024
28f0313
remove null
pinguin3245678 Oct 14, 2024
65a46a6
undo
pinguin3245678 Oct 14, 2024
0948c77
Update src/main/java/org/openrewrite/staticanalysis/EqualsAvoidsNullV…
Oct 14, 2024
32e089e
add doc
pinguin3245678 Oct 14, 2024
29ad521
Merge remote-tracking branch 'origin/EqualsAvoidsNullVisitor-extend-l…
pinguin3245678 Oct 14, 2024
4cc0405
fix getSuperIfSelectNull
pinguin3245678 Oct 14, 2024
5c54ac0
paranthese
pinguin3245678 Oct 14, 2024
b70d866
fix dry
pinguin3245678 Oct 14, 2024
0ee53b4
fix if
pinguin3245678 Oct 14, 2024
cae686e
fix if
pinguin3245678 Oct 14, 2024
cbe6400
fix literalsFirstInComparisons
pinguin3245678 Oct 14, 2024
f4e2a59
order
pinguin3245678 Oct 14, 2024
82ba719
dry firstArgument
pinguin3245678 Oct 14, 2024
34ced7e
naming
pinguin3245678 Oct 14, 2024
7767c42
Merge branch 'main' into EqualsAvoidsNullVisitor-extend-literalsfirst…
Oct 16, 2024
4ec7e3b
Update src/main/java/org/openrewrite/staticanalysis/EqualsAvoidsNullV…
Oct 16, 2024
d279932
Fix earlier incorrect bot suggestion
timtebeek Oct 21, 2024
3e715de
Place binary operators on same line; remove nested ternaries
timtebeek Oct 21, 2024
a2c3c1f
Restore more deliberate naming of `potentialNullCheck`
timtebeek Oct 21, 2024
dd458ab
Collapse imports as per what's common elsewhere
timtebeek Oct 21, 2024
9fed309
Make the method matchers static final again
timtebeek Oct 21, 2024
2a582a1
add leftover
pinguin3245678 Oct 22, 2024
dadb102
Merge remote-tracking branch 'origin/EqualsAvoidsNullVisitor-extend-l…
pinguin3245678 Oct 22, 2024
726c5b0
Use JSpecify nullable annotations
timtebeek Oct 22, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,68 +26,112 @@
import org.openrewrite.marker.Markers;

import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;

/**
* A visitor that identifies and addresses potential issues related to
* the use of {@code equals} methods in Java, particularly to avoid
* null pointer exceptions when comparing strings.
* <p>
* This visitor looks for method invocations of {@code equals},
* {@code equalsIgnoreCase}, {@code compareTo}, and {@code contentEquals},
* and performs optimizations to ensure null checks are correctly applied.
* <p>
* For more details, refer to the PMD best practices:
* <a href="https://pmd.github.io/pmd/pmd_rules_java_bestpractices.html#LiteralsFirstInComparisons">Literals First in Comparisons</a>
*
* @param <P> The type of the parent context used for visiting the AST.
*/
@Value
@EqualsAndHashCode(callSuper = false)
public class EqualsAvoidsNullVisitor<P> extends JavaVisitor<P> {
Copy link
Contributor Author

@Pankraz76 Pankraz76 Oct 17, 2024

Choose a reason for hiding this comment

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

Perhaps LiteralsFirstInComparisonsVisitor would be a better fit to align with the corresponding PMD rule name already in place, as it's no longer just about equality; both equality and comparison are now merely implementation details.

Suggested change
public class EqualsAvoidsNullVisitor<P> extends JavaVisitor<P> {
public class LiteralsFirstInComparisonsVisitor<P> extends JavaVisitor<P> {

Copy link
Member

@timtebeek timtebeek Oct 21, 2024

Choose a reason for hiding this comment

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

I'd perhaps hold off on renaming this just yet. While the visitor only appears to be used in this module, the recipe name is well established as part of our Common static analysis issues recipe, which folks have created custom copies off that might break if we similarly rename the recipe. I'll think this over as I go through the review.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes thanks. Its just an idea and of course would be a new PR topic.

private static final MethodMatcher STRING_EQUALS = new MethodMatcher("String equals(java.lang.Object)");
private static final MethodMatcher STRING_EQUALS_IGNORE_CASE = new MethodMatcher("String equalsIgnoreCase(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 j = super.visitMethodInvocation(method, p);
if (!(j instanceof J.MethodInvocation)) {
return j;
J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, p);
if (m.getSelect() != null &&
!(m.getSelect() instanceof J.Literal) &&
m.getArguments().get(0) instanceof J.Literal &&
isStringComparisonMethod(m)) {
return literalsFirstInComparisonsBinaryCheck(m, getCursor().getParentTreeCursor().getValue());
}
J.MethodInvocation m = (J.MethodInvocation) j;
if (m.getSelect() == null) {
return m;
return m;
}

private boolean isStringComparisonMethod(J.MethodInvocation methodInvocation) {
return EQUALS.matches(methodInvocation) ||
!style.getIgnoreEqualsIgnoreCase() &&
EQUALS_IGNORE_CASE.matches(methodInvocation) ||
COMPARE_TO.matches(methodInvocation) ||
COMPARE_TO_IGNORE_CASE.matches(methodInvocation) ||
CONTENT_EQUALS.matches(methodInvocation);
}

private Expression literalsFirstInComparisonsBinaryCheck(J.MethodInvocation m, P parent) {
if (parent instanceof J.Binary) {
handleBinaryExpression(m, (J.Binary) parent);
}
return getExpression(m, m.getArguments().get(0));
}

if ((STRING_EQUALS.matches(m) || (!Boolean.TRUE.equals(style.getIgnoreEqualsIgnoreCase()) && STRING_EQUALS_IGNORE_CASE.matches(m))) &&
m.getArguments().get(0) instanceof J.Literal &&
!(m.getSelect() instanceof J.Literal)) {
Tree parent = getCursor().getParentTreeCursor().getValue();
if (parent instanceof J.Binary) {
J.Binary binary = (J.Binary) parent;
if (binary.getOperator() == J.Binary.Type.And && binary.getLeft() instanceof J.Binary) {
J.Binary potentialNullCheck = (J.Binary) binary.getLeft();
if ((isNullLiteral(potentialNullCheck.getLeft()) && matchesSelect(potentialNullCheck.getRight(), m.getSelect())) ||
(isNullLiteral(potentialNullCheck.getRight()) && matchesSelect(potentialNullCheck.getLeft(), m.getSelect()))) {
doAfterVisit(new RemoveUnnecessaryNullCheck<>(binary));
}
}
}
private static Expression getExpression(J.MethodInvocation m, Expression firstArgument) {
return firstArgument.getType() == JavaType.Primitive.Null ?
literalsFirstInComparisonsNull(m, firstArgument) :
literalsFirstInComparisons(m, firstArgument);
}

private static J.Binary literalsFirstInComparisonsNull(J.MethodInvocation m, Expression firstArgument) {
return new J.Binary(Tree.randomId(),
m.getPrefix(),
Markers.EMPTY,
requireNonNull(m.getSelect()),
JLeftPadded.build(J.Binary.Type.Equal).withBefore(Space.SINGLE_SPACE),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Build maybe okay, but the Space is DRY Space. SPACE

If i need unwanted context i move my mouse and not clutter the code with redundancy, but its a style where most dev´s use full qualified imports.

image

Copy link
Member

Choose a reason for hiding this comment

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

Often classes use a mix of the two constants in Space, only one of which has _SPACE in the name for the repetition. I'd prefer then to be consistent with prior use, and i/fwhen we dedice to change that, do so through our enforced patterns in rewrite-recommendations. I hope that context helps!

Copy link
Contributor

Choose a reason for hiding this comment

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

I suspect the idea with the name was to emphasize that the object represents a single space character. The class Space represents any sequence of whitespace and comments, so Space.SINGLE would be a bit ambiguous (a single space, tab, newline, or comment?), so Space.SINGLE_SPACE uses "space" in two different meanings.

firstArgument.withPrefix(Space.SINGLE_SPACE),
JavaType.Primitive.Boolean);
}

if (m.getArguments().get(0).getType() == JavaType.Primitive.Null) {
return new J.Binary(Tree.randomId(), m.getPrefix(), Markers.EMPTY,
m.getSelect(),
JLeftPadded.build(J.Binary.Type.Equal).withBefore(Space.SINGLE_SPACE),
m.getArguments().get(0).withPrefix(Space.SINGLE_SPACE),
JavaType.Primitive.Boolean);
} else {
m = m.withSelect(((J.Literal) m.getArguments().get(0)).withPrefix(m.getSelect().getPrefix()))
.withArguments(singletonList(m.getSelect().withPrefix(Space.EMPTY)));
private static J.MethodInvocation literalsFirstInComparisons(J.MethodInvocation m, Expression firstArgument) {
return m.withSelect(firstArgument.withPrefix(requireNonNull(m.getSelect()).getPrefix()))
.withArguments(singletonList(m.getSelect().withPrefix(Space.EMPTY)));
}

private void handleBinaryExpression(J.MethodInvocation m, J.Binary binary) {
if (binary.getOperator() == J.Binary.Type.And && binary.getLeft() instanceof J.Binary) {
J.Binary potentialNullCheck = (J.Binary) binary.getLeft();
if (isNullLiteral(potentialNullCheck.getLeft()) && matchesSelect(potentialNullCheck.getRight(), requireNonNull(m.getSelect())) ||
isNullLiteral(potentialNullCheck.getRight()) && matchesSelect(potentialNullCheck.getLeft(), requireNonNull(m.getSelect()))) {
doAfterVisit(new RemoveUnnecessaryNullCheck<>(binary));
}
}

return m;
}

private boolean isNullLiteral(Expression expression) {
return expression instanceof J.Literal && ((J.Literal) expression).getType() == JavaType.Primitive.Null;
}

private boolean matchesSelect(Expression expression, Expression select) {
return expression.printTrimmed(getCursor()).replaceAll("\\s", "").equals(select.printTrimmed(getCursor()).replaceAll("\\s", ""));
return expression.printTrimmed(getCursor()).replaceAll("\\s", "")
.equals(select.printTrimmed(getCursor()).replaceAll("\\s", ""));
}

private static class RemoveUnnecessaryNullCheck<P> extends JavaVisitor<P> {

private final J.Binary scope;

boolean done;

public RemoveUnnecessaryNullCheck(J.Binary scope) {
this.scope = scope;
}

@Override
public @Nullable J visit(@Nullable Tree tree, P p) {
if (done) {
Expand All @@ -96,17 +140,12 @@ private static class RemoveUnnecessaryNullCheck<P> extends JavaVisitor<P> {
return super.visit(tree, p);
}

public RemoveUnnecessaryNullCheck(J.Binary scope) {
this.scope = scope;
}

@Override
public J visitBinary(J.Binary binary, P p) {
if (scope.isScope(binary)) {
done = true;
return binary.getRight().withPrefix(Space.EMPTY);
}

return super.visitBinary(binary, p);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@

import java.time.Duration;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -90,7 +89,7 @@ public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx)
return invocation; // Might contain special characters; unsafe to replace
}
String secondValue = (String) ((J.Literal) secondArgument).getValue();
if (Objects.nonNull(secondValue) && (secondValue.contains("$") || secondValue.contains("\\"))) {
if (secondValue != null && (secondValue.contains("$") || secondValue.contains("\\"))) {
return invocation; // Does contain special characters; unsafe to replace
}

Expand All @@ -100,7 +99,7 @@ public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx)
// Checks if the String literal may not be a regular expression,
// if so, then change the method invocation name
String firstValue = (String) ((J.Literal) firstArgument).getValue();
if (Objects.nonNull(firstValue) && !mayBeRegExp(firstValue)) {
if (firstValue != null && !mayBeRegExp(firstValue)) {
String unEscapedLiteral = unEscapeCharacters(firstValue);
invocation = invocation
.withName(invocation.getName().withSimpleName("replace"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public class A {
String s = null;
if(s.equals("test")) {}
if(s.equalsIgnoreCase("test")) {}
System.out.println(s.compareTo("test"));
System.out.println(s.compareToIgnoreCase("test"));
System.out.println(s.contentEquals("test"));
}
}
""",
Expand All @@ -51,6 +54,9 @@ public class A {
String s = null;
if("test".equals(s)) {}
if("test".equalsIgnoreCase(s)) {}
System.out.println("test".compareTo(s));
System.out.println("test".compareToIgnoreCase(s));
System.out.println("test".contentEquals(s));
}
}
"""
Expand Down