Skip to content

JS-1421 Fix FP in S2234: suppress intentional comparator argument reversal#6590

Open
sonar-nigel[bot] wants to merge 11 commits intomasterfrom
fix/JS-1421-fix-fp-on-s2234-intentional-argument-reversal-in-comparator-wrapper-functions-sonnet
Open

JS-1421 Fix FP in S2234: suppress intentional comparator argument reversal#6590
sonar-nigel[bot] wants to merge 11 commits intomasterfrom
fix/JS-1421-fix-fp-on-s2234-intentional-argument-reversal-in-comparator-wrapper-functions-sonnet

Conversation

@sonar-nigel
Copy link
Contributor

@sonar-nigel sonar-nigel bot commented Mar 13, 2026

S2234 was incorrectly flagging the common pattern of a 2-parameter wrapper function that intentionally reverses its arguments to produce a descending comparator, e.g. (a, b) => compare(b, a).

Changes

  • Fix false positive: Added isIntentionalComparatorReversal() in rule.ts that detects when a call site is the direct body or sole return of a 2-parameter enclosing function with its parameters swapped. If matched, the issue is suppressed.
  • Tests: Added test cases covering arrow function expression body, function expression with sole return, and inline sort callback. Boundary cases verify the exception only applies when exactly 2 parameters are reversed with no extra statements.
  • Ruling files: Synced expected ruling files to reflect the reduced false-positive rate.
  • Cleanup: Removed three redundant as type assertions from isIntentionalComparatorReversal() that TypeScript 5.5+ infers automatically via type predicates and discriminated union narrowing.
  • Docs: Updated rspec to document the new compliant code exception under diff-id=1.

Relates to JS-1421

Proposed rspec changes
diff --git a/rules/S2234/javascript/rule.adoc b/rules/S2234/javascript/rule.adoc
index 07681f4..496925d 100644
--- a/rules/S2234/javascript/rule.adoc
+++ b/rules/S2234/javascript/rule.adoc
@@ -56,6 +56,14 @@ function doTheThing() {
 }
 ----
 
+Intentional comparator reversal is also ignored. When the entire body of a 2-parameter wrapper function is a single call whose two arguments are exactly the enclosing function's two parameters in reversed order, the swap is treated as a deliberate reverse-order wrapper — a common pattern for descending sort comparators:
+
+[source,javascript,diff-id=1,diff-type=compliant]
+----
+const items = ['banana', 'apple', 'cherry'];
+items.sort((a, b) => compareStrings(b, a)); // Compliant: intentional descending comparator
+----
+
 == Resources
 === Documentation

Vibe Bot and others added 4 commits March 13, 2026 17:39
Tests cover the comparator-reversal pattern where an enclosing
2-parameter function's sole body is a call with its parameters
in reversed order (e.g. `(a, b) => compare(b, a)`). This is a
legitimate descending sort comparator, not a bug.

Three valid cases are added: arrow function expression body,
function expression with sole return statement, and inline sort
callback. Two invalid boundary cases verify the exception is
narrow: a 3-parameter wrapper and a block body with extra
statements.

Relates to JS-1421
S2234 was incorrectly flagging the common pattern of a 2-parameter
wrapper function that intentionally reverses its arguments to produce
a descending comparator, e.g. `(a, b) => compare(b, a)`.

Added `isIntentionalComparatorReversal()` inside the `create` closure
in rule.ts. It is called before `raiseIssue` in `checkArguments`. The
check finds the immediately enclosing ArrowFunctionExpression or
FunctionExpression, confirms it has exactly 2 identifier parameters
whose names match the two swapped arguments, and confirms the call is
the direct expression body or the sole return statement of that
function. If all conditions hold, the issue is suppressed.

Also updated rspec to document the new exception as a compliant code
block under diff-id=1, following the same style as the existing
if-statement exception.

Relates to JS-1421
Ticket: JS-1421

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Remove three unnecessary type assertions (typescript:S4325) from the
isIntentionalComparatorReversal() function in rule.ts. TypeScript 5.5+
infers these types via inferred type predicates and discriminated union
narrowing, making the explicit `as` casts redundant:
- Removed `as ArrowFunctionExpression | FunctionExpression | undefined`
  from the .find() result (inferred type predicates in Array.prototype.find)
- Removed `as Expression | BlockStatement` from enclosingFunc.body
  (inferred from the narrowed ArrowFunctionExpression | FunctionExpression union)
- Removed `as ReturnStatement` inside the if-condition (TypeScript narrows
  body.body[0] after the .type === 'ReturnStatement' discriminant check)
@github-actions
Copy link
Contributor

github-actions bot commented Mar 13, 2026

Ruling Report

No changes to ruling expected issues in this PR

@sonar-nigel sonar-nigel bot marked this pull request as ready for review March 13, 2026 18:17
@sonar-nigel sonar-nigel bot requested a review from a team March 13, 2026 18:17
@francois-mora-sonarsource
Copy link
Contributor

The two early-exit guards in isIntentionalComparatorReversal() can be merged into one:

if (!enclosingFunc || enclosingFunc.params.length !== 2) {
  return false;
}

Comment: The two early-exit guards in `isIntentionalComparatorReversal()` can be merged into one:

```ts
if (!enclosingFunc || enclosingFunc.params.length !== 2) {
  return false;
}
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@sonar-nigel
Copy link
Contributor Author

sonar-nigel bot commented Mar 17, 2026

Done, merged the two guards into one:

if (!enclosingFunc || enclosingFunc.params.length !== 2) {
  return false;
}

@francois-mora-sonarsource
Copy link
Contributor

LGTM - fixes S2234 false positives when a 2-parameter wrapper function intentionally reverses its arguments (e.g. descending sort comparators like (a, b) => compare(b, a)) — #6590

@github-actions
Copy link
Contributor

README Freshness Check

The rules README is out of date.

A fix PR has been created: #6622

Please review and merge it into your branch.

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@sonar-nigel
Copy link
Contributor Author

sonar-nigel bot commented Mar 17, 2026

Thank you for the review!

Address external SonarQube issue typescript:S6582 in rule.ts line 156.
Replace the `!enclosingFunc || enclosingFunc.params.length !== 2` pattern
with the more concise optional chain expression
`enclosingFunc?.params.length !== 2` in `isIntentionalComparatorReversal()`.
The semantics are identical: the condition is truthy (triggering early return)
when the enclosing function is absent or does not have exactly 2 parameters.
s2234-intentional-argument-reversal-in-comparator-wrapper-functions-sonnet

# Conflicts:
#	packages/jsts/src/rules/S2234/rule.ts
#	packages/jsts/src/rules/S2234/unit.test.ts
Copy link

@sonar-review-alpha sonar-review-alpha bot left a comment

Choose a reason for hiding this comment

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

Conclusion: The implementation is clean and well-tested, but there's one design concern worth discussing before merge: the suppression is broader than its name implies and could introduce false negatives for non-comparator functions.

🗣️ Give feedback

…ules/S2234/rule.ts

Comment: The function is named `isIntentionalComparatorReversal`, but it suppresses **any** 2-param wrapper that solely calls another function with reversed args — it has no way to know whether the wrapped function is actually a comparator.

This means a genuine bug like:
```js
function formatDate(year, month) { return `${year}-${month}`; }
const wrongWrapper = (year, month) => formatDate(month, year); // ← real bug, now silenced
```
…will be silently suppressed. S2234 would correctly flag this today; after this change it won't.

Is this tradeoff intentional? If the accepted position is "a sole-reversal wrapper is *always* intentional regardless of the callee", that should be documented in the function comment. If the intent really is to limit suppression to comparator-style functions, a narrower heuristic is needed — for example, only suppress when the enclosing function is passed directly as an argument to a known sort-accepting method (`.sort()`, `.toSorted()`, etc.).

- [ ] Mark as noise
@sonar-nigel
Copy link
Contributor Author

sonar-nigel bot commented Mar 18, 2026

Thanks for the summary. The design concern you raise was already flagged in the previous round of review, and the accepted tradeoff is now explicitly documented in the JSDoc on isIntentionalComparatorReversal() (lines 144–159):

Accepted tradeoff: this check fires whenever the enclosing 2-parameter function's entire body is that reversed call — it does not verify that the callee is a comparator or that the wrapper is passed to .sort() / .toSorted(). The rationale is that a sole-purpose reversal wrapper is almost always intentional (descending sort, RTL/LTR flip, semver reverse-compare, etc.), and restricting to sort-accepting methods would miss the majority of real-world cases. A genuine bug like (year, month) => formatDate(month, year) is unlikely to be the only statement in the wrapper function, so the sole-body constraint limits false negatives in practice.

The ruling data (8 FPs across kibana, JupyterLab, ESLint, Blueprint, semver) shows that the pattern arises broadly beyond .sort() call sites, which is why narrowing to sort-accepting methods was deliberately rejected. The sole-body constraint is the practical guard against the false-negative scenario you describe.

Copy link

@sonar-review-alpha sonar-review-alpha bot left a comment

Choose a reason for hiding this comment

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

LGTM! ✅

🗣️ Give feedback

@sonar-nigel
Copy link
Contributor Author

sonar-nigel bot commented Mar 18, 2026

Thanks for the LGTM! ✅

@sonarqube-next
Copy link

Copy link

@sonar-review-alpha sonar-review-alpha bot left a comment

Choose a reason for hiding this comment

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

LGTM! ✅

🗣️ Give feedback

@sonar-nigel
Copy link
Contributor Author

sonar-nigel bot commented Mar 18, 2026

Thank you for the review!

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.

1 participant