Skip to content

SONARJAVA-6790: Implement S9350: equals() implementations should not compare mismatched members - #5946

Open
nathsou wants to merge 9 commits into
masterfrom
new-rule/S9350
Open

SONARJAVA-6790: Implement S9350: equals() implementations should not compare mismatched members#5946
nathsou wants to merge 9 commits into
masterfrom
new-rule/S9350

Conversation

@nathsou

@nathsou nathsou commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Implement S9350 in SonarJava as a native check that flags equals() implementations comparing unmatched instance fields or getters of the enclosing type.
  • Cover ==/!=, Objects.equals, Guava Objects.equal, Arrays.equals, instance equals, and 0-arg getters/record accessors, including the swapped-pair exception for order-independent equality.

Links

AI disclosure

  • LLM model used for implementation: cursor-grok-4.6-high

@nathsou nathsou self-assigned this Aug 18, 2026
@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

SONARJAVA-6790

Comment on lines +94 to +101
Map<Tree, Set<MemberPair>> pairsByStatement = new HashMap<>();
for (ComparisonSite comparison : collector.comparisons) {
pairsByStatement.computeIfAbsent(comparison.statement, key -> new HashSet<>()).add(comparison.pair());
}
for (ComparisonSite comparison : collector.comparisons) {
if (pairsByStatement.get(comparison.statement).contains(comparison.pair().reversed())) {
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Edge Case: Per-statement swap exception misses order-independent equality split across statements

The swapped-pair exception is now scoped per enclosing statement (pairsByStatement.get(comparison.statement)), so an order-independent equality whose forward and reversed comparisons live in different statements will no longer be recognized as symmetric and will be reported as a false positive. Example: boolean f = this.a == that.b; on one line and boolean b = this.b == that.a; on another produces two issues even though together they express symmetric equality. This is uncommon, but if you want to preserve suppression for such cases consider also matching reversed pairs found anywhere in the same method while keeping the per-statement grouping for the DistinctStatements case.

Was this helpful? React with 👍 / 👎

@github-actions

Copy link
Copy Markdown
Contributor

Ruling needs updating. A fix PR has been created: #5948

Please review and merge it into your branch.

Ruling flagged this.foo.equals(that.foo) on generic types because JSymbol equality includes type arguments, so the same field looked mismatched.

@romainbrenguier romainbrenguier left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It needs a test without semantic, otherwise it looks ok

Comment on lines +29 to +32
CheckVerifier.newVerifier()
.onFile(mainCodeSourcesPath("checks/EqualsMismatchedMembersCheckSample.java"))
.withCheck(new EqualsMismatchedMembersCheck())
.verifyIssues();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should have a similar test using .withoutSemantic

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added test_without_semantic() on the compiling sample. The check still reports the same issues from intra-file symbols when no bytecode is provided, so this uses .withoutSemantic().verifyIssues() like SillyEqualsCheckTest.

@gitar-bot

gitar-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 3 resolved / 4 findings

Implements rule S9350 to flag equals() implementations comparing mismatched members with comprehensive test coverage and defensive branch handling. Consider expanding the per-statement swap exception to catch order-independent equality splits across multiple statements.

💡 Edge Case: Per-statement swap exception misses order-independent equality split across statements

📄 java-checks/src/main/java/org/sonar/java/checks/EqualsMismatchedMembersCheck.java:94-101 📄 java-checks/src/main/java/org/sonar/java/checks/EqualsMismatchedMembersCheck.java:269-275

The swapped-pair exception is now scoped per enclosing statement (pairsByStatement.get(comparison.statement)), so an order-independent equality whose forward and reversed comparisons live in different statements will no longer be recognized as symmetric and will be reported as a false positive. Example: boolean f = this.a == that.b; on one line and boolean b = this.b == that.a; on another produces two issues even though together they express symmetric equality. This is uncommon, but if you want to preserve suppression for such cases consider also matching reversed pairs found anywhere in the same method while keeping the per-statement grouping for the DistinctStatements case.

✅ 3 resolved
Edge Case: Mismatched-member check ignores which instance operands belong to

📄 java-checks/src/main/java/org/sonar/java/checks/EqualsMismatchedMembersCheck.java:145-159 📄 java-checks/src/main/java/org/sonar/java/checks/EqualsMismatchedMembersCheck.java:46-47
addIfDubious/member extract only the field or getter symbol and never verify that one operand is accessed on this while the other is accessed on the compared parameter. As a result, a comparison of two different members of the same instance is flagged as a violation. For example this.start == this.end or that.start == that.end inside equals (a legitimate normalization/guard check) is reported as "pairing start with end breaks the equality contract", a false positive. The secondary location labels (SECONDARY_THIS="Compared member on this", SECONDARY_OTHER="...on the other instance") also become inaccurate in these cases. Consider only pairing when the two operands reference different instances (one on this/implicit, the other on the pattern/parameter variable) before recording a ComparisonSite.

Quality: MemberRef.symbol component is now unused after name-based switch

📄 java-checks/src/main/java/org/sonar/java/checks/EqualsMismatchedMembersCheck.java:185-199 📄 java-checks/src/main/java/org/sonar/java/checks/EqualsMismatchedMembersCheck.java:285
This commit removed the last reads of MemberRef.symbol (comparison in addIfDubious and pair() now use displayName), leaving the symbol record component dead — it is still populated in field()/getter() but never read. Drop the symbol component from the MemberRef record (and its constructor arguments) to remove the dead state and clarify that member identity is now name-based.

Edge Case: Name-based equal fallback can match unrelated 2-arg methods

📄 java-checks/src/main/java/org/sonar/java/checks/EqualsMismatchedMembersCheck.java:192-202
isTwoArgEqualityHelper falls back to matching any static-or-unknown method literally named equal taking 2 arguments. A user-defined static helper named equal(x, y) that is not an equality comparison would be treated as one, so equal(this.a, that.b) would be flagged even though the method has nothing to do with equality. Consider restricting the fallback to known equality helper types, or requiring a boolean return type, to reduce false positives.

🤖 Prompt for agents
Code Review: Implements rule S9350 to flag `equals()` implementations comparing mismatched members with comprehensive test coverage and defensive branch handling. Consider expanding the per-statement swap exception to catch order-independent equality splits across multiple statements.

1. 💡 Edge Case: Per-statement swap exception misses order-independent equality split across statements
   Files: java-checks/src/main/java/org/sonar/java/checks/EqualsMismatchedMembersCheck.java:94-101, java-checks/src/main/java/org/sonar/java/checks/EqualsMismatchedMembersCheck.java:269-275

   The swapped-pair exception is now scoped per enclosing statement (`pairsByStatement.get(comparison.statement)`), so an order-independent equality whose forward and reversed comparisons live in different statements will no longer be recognized as symmetric and will be reported as a false positive. Example: `boolean f = this.a == that.b;` on one line and `boolean b = this.b == that.a;` on another produces two issues even though together they express symmetric equality. This is uncommon, but if you want to preserve suppression for such cases consider also matching reversed pairs found anywhere in the same method while keeping the per-statement grouping for the DistinctStatements case.

Implementation Status ✅ 1 / 1 issues implemented
SONARJAVA-6790 — 1 / 1 objectives

The PR successfully implements rule S9350 with comprehensive unit tests and documentation to detect mismatched members in equals() implementations.

✅ 1 complete
  • ✅ Implement rule S9350 to detect when equals() implementations compare mismatched members
Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqube-next

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants