-
Notifications
You must be signed in to change notification settings - Fork 92
address: LiteralsFirstInComparisons PMD rule #368
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
Changes from all commits
78eb2e7
db907e9
44432a0
6bbb603
3b95e60
b6f48c8
b187593
735ca39
111baaa
709a488
983b7f5
d9f83f6
5b30904
13d7057
cc2c022
e94ff44
8addad3
21f37a4
e424b6f
9470cd4
35ed8cf
a519472
2f0e9b8
931755a
2c2b33a
7e813dc
241262f
4d78d78
ca9d3d7
55b8299
bec2a8e
477820a
e1cdc55
0f85697
f2b94d4
396d43b
6c205af
586d85e
d1b6c1d
0715f12
28f0313
65a46a6
0948c77
32e089e
29ad521
4cc0405
5c54ac0
b70d866
0ee53b4
cae686e
cbe6400
f4e2a59
82ba719
34ced7e
7767c42
4ec7e3b
d279932
3e715de
a2c3c1f
dd458ab
9fed309
2a582a1
dadb102
726c5b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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> { | ||
| 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), | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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) { | ||
|
|
@@ -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); | ||
| } | ||
| } | ||
|
|
||

Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.