Skip to content

JS-1466 Fix S1121 false positives for idiomatic assignment patterns#6643

Open
sonar-nigel[bot] wants to merge 7 commits intomasterfrom
fix/JS-1466-fix-fp-on-s1121-assignment-in-conditional-expressions-for-idiomatic-patterns-sonnet
Open

JS-1466 Fix S1121 false positives for idiomatic assignment patterns#6643
sonar-nigel[bot] wants to merge 7 commits intomasterfrom
fix/JS-1466-fix-fp-on-s1121-assignment-in-conditional-expressions-for-idiomatic-patterns-sonnet

Conversation

@sonar-nigel
Copy link
Contributor

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

Fixes false positives in rule S1121 (assignment in conditional expressions) for three common JavaScript idioms where assignment in an expression is intentional.

Changes

  • Fix: Added guard functions isIfCondition, isForCondition, and isReturnStatement to rule.ts to exempt idiomatic assignment patterns from the rule, mirroring the existing isWhileCondition and isForInitOrUpdate guards.
  • Tests: Added test cases for the newly valid patterns and additional invalid patterns drawn from ruling analysis.
  • RSPEC: Updated exceptions list and code examples to reflect the new allowed patterns.

Idiomatic patterns now allowed

  • if (m = regex.exec(src)) — assignment as the direct test of an if statement
  • for (; node = arr[i]; i++) — assignment as the test condition of a for loop
  • return (cache[key] = fn()) — assignment in a return statement (e.g. memoization)

Ruling validation

Ruling analysis confirmed zero mismatches across 997 entries: 560 previously-raised issues are now correctly suppressed (idiomatic return, if-test, and for-test patterns), and 437 issues continue to be raised as true positives (assignments in function arguments, ternary branches, nested logical expressions, and unary operators).

Fixes JS-1466

Proposed rspec changes
diff --git a/rules/S1121/javascript/rule.adoc b/rules/S1121/javascript/rule.adoc
index 2d58f7b..16e47e4 100644
--- a/rules/S1121/javascript/rule.adoc
+++ b/rules/S1121/javascript/rule.adoc
@@ -14,6 +14,8 @@ The rule does not raise issues for the following patterns:
 * assignments in lambda body: ``++() => a = 0++``
 * conditional assignment idiom: ``++a || (a = 0)++``
 * assignments in (do-)while conditions: ``++while (a = 0);++``
+* assignments as the direct test of an ``++if++`` or ``++for++`` statement: ``++if (m = regex.exec(src))++``, ``++for (; node = arr[i]; i++)++``
+* assignments in ``++return++`` statements: ``++return (cache[key] = fn());++``
 
 == How to fix it
 
@@ -25,19 +27,15 @@ include::../how-to-fix-it.adoc[]
 
 [source,javascript,diff-id=1,diff-type=noncompliant]
 ----
-if (val = value() && check()) { // Noncompliant
-  // ...
-}
+processValue(a = getValue()); // Noncompliant
 ----
 
 ==== Compliant solution
 
 [source,javascript,diff-id=1,diff-type=compliant]
 ----
-val = value();
-if (val && check()) {
-  // ...
-}
+a = getValue();
+processValue(a);
 ----
 
 include::../see.adoc[]

Vibe Bot added 4 commits March 19, 2026 13:30
Tests cover scenarios where assignments in conditional expressions are
intentional JavaScript idioms: assignments as the direct test of an if
statement (e.g. regex matching), assignments as the test condition of a
for loop (e.g. array traversal), and assignments in return statements
(e.g. memoization). Seven previously-invalid test cases that become
valid under the fix are moved to the valid array, and nine new test
cases are added to document the idiomatic patterns.

Relates to JS-1466
S1121 was raising false positives for three common JavaScript idioms
where assignment in an expression is intentional: assignments used
directly as the test of an `if` statement (e.g., `if (m = regex.exec(src))`),
assignments used as the test condition of a `for` loop
(e.g., `for (; node = arr[i]; i++)`), and assignments in `return`
statements (e.g., `return (cache[key] = fn())`).

Three new guard functions are added to `rule.ts` — `isIfCondition`,
`isForCondition`, and `isReturnStatement` — mirroring the existing
pattern of `isWhileCondition` and `isForInitOrUpdate`. The rspec
exceptions list and code example are updated accordingly.

Relates to JS-1466
Ruling analysis confirmed the implementation is correct with zero mismatches
across 997 entries: 560 previously-raised issues are now correctly suppressed
(idiomatic return, if-test, and for-test assignment patterns), and 437 issues
continue to be raised as true positives (assignments in function arguments,
ternary branches, nested logical expressions, and unary operators).

Enhanced tests with four new cases drawn from ruling patterns:
- Valid: compound assignment in return (e.g., `return (carry += value)`)
- Valid: node-traversal for-loop test (e.g., `for (var i = 0; node = nodes[i++];)`)
- Invalid: assignment wrapped in unary NOT in if condition (`if (!(a = getValue()))`)
- Invalid: assignment inside a function call in if condition (`if (isDefined(a = getValue()))`)
@github-actions
Copy link
Contributor

github-actions bot commented Mar 19, 2026

Ruling Report

Code no longer flagged (561 issues)

S1121

Ghost/core/client/app/utils/ghost-paths.js:13

    11 |     });
    12 | 
>   13 |     return route += '/';
    14 | };
    15 | 

Joust/ts/components/game/Hero.tsx:55

    53 | 				name = this.props.cards.get(name).name || name;
    54 | 			}
>   55 | 			return title += name;
    56 | 		}, "");
    57 | 

TypeScript/lib/tsc.js:4221

  4219 |                 }
  4220 |             }
> 4221 |             return token = 71;
  4222 |         }
  4223 |         function scanBinaryOrOctalDigits(base) {

TypeScript/lib/tsc.js:4251

  4249 |                 tokenPos = pos;
  4250 |                 if (pos >= end) {
> 4251 |                     return token = 1;
  4252 |                 }
  4253 |                 var ch = text.charCodeAt(pos);

TypeScript/lib/tsc.js:4260

  4258 |                     }
  4259 |                     else {
> 4260 |                         return token = 6;
  4261 |                     }
  4262 |                 }

TypeScript/lib/tsc.js:4278

  4276 |                                 pos++;
  4277 |                             }
> 4278 |                             return token = 4;
  4279 |                         }
  4280 |                     case 9:

TypeScript/lib/tsc.js:4292

  4290 |                                 pos++;
  4291 |                             }
> 4292 |                             return token = 5;
  4293 |                         }
  4294 |                     case 33:

TypeScript/lib/tsc.js:4302

  4300 |                         }
  4301 |                         pos++;
> 4302 |                         return token = 51;
  4303 |                     case 34:
  4304 |                     case 39:

TypeScript/lib/tsc.js:4306

  4304 |                     case 39:
  4305 |                         tokenValue = scanString();
> 4306 |                         return token = 9;
  4307 |                     case 96:
  4308 |                         return token = scanTemplateAndSetTokenValue();

TypeScript/lib/tsc.js:4308

  4306 |                         return token = 9;
  4307 |                     case 96:
> 4308 |                         return token = scanTemplateAndSetTokenValue();
  4309 |                     case 37:
  4310 |                         if (text.charCodeAt(pos + 1) === 61) {

...and 551 more

📋 View full report

Code no longer flagged (561)

S1121

@sonar-nigel sonar-nigel bot marked this pull request as ready for review March 19, 2026 14:21
@sonar-nigel sonar-nigel bot requested a review from a team March 19, 2026 14:21
@sonar-review-alpha
Copy link

sonar-review-alpha bot commented Mar 19, 2026

Summary

Rule S1121 now exempts three idiomatic assignment patterns where assignment in an expression is intentional and readable:

  1. Direct test conditions: if (m = regex.exec(src)) — assignments as the sole test in if/while/do-while/for statements
  2. Return statements: return (cache[key] = fn()) — assignments in return expressions (common for memoization)
  3. Refactored logic: Consolidated while/for/if condition checks into a single isDirectTestCondition() helper

The change eliminates ~560 false positives from the TypeScript/JavaScript ecosystem (confirmed via ruling analysis), while maintaining 437 legitimate violations in patterns like function arguments, ternary branches, and nested logical expressions.

What reviewers should know

Code review focus: packages/jsts/src/rules/S1121/rule.ts

  • The old isWhileCondition() was replaced by a unified isDirectTestCondition() that covers if/while/do-while/for — this consolidates 4 similar patterns into one
  • Added isReturnStatement() which is simpler but crucial for memoization idioms
  • The guard order in the main check was reordered slightly; logic is equivalent

Test changes: unit.test.ts shows the policy shift clearly:

  • Many tests moved from invalidvalid arrays (lines 124–154), documenting the now-allowed patterns
  • New invalid cases added (lines 197–203) to catch the problematic scenarios: assignments buried in logical operators (&&, ||), unary operators (!), or function calls — these remain violations because they're harder to read
  • Note the distinction: if (m = fn()) is now valid, but if (!(m = fn())) or if (checkIfDefined(m = fn())) are still invalid

Ruling cleanup: The large JSON diffs are expected — ~560 line numbers removed from expected issues across multiple test projects. These represent idiomatic patterns now correctly recognized as non-violations. Spot-check one or two projects if concerned about legitimacy.


  • Generate Walkthrough
  • Generate Diagram

🗣️ Give feedback

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: Clean, well-scoped fix. The ruling snapshots give high confidence the precision improvement is correct.

🗣️ Give feedback

return parent.type === 'ReturnStatement';
}

function isIfCondition(parent: estree.Node, expr: estree.AssignmentExpression) {

Choose a reason for hiding this comment

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

Logic duplication: isIfCondition and isForCondition (line 124) both implement parent.type === 'XStatement' && parent.test === expr, which is structurally identical to the existing isWhileCondition. All three functions are now parallel — any future behavioral change to the "direct test condition" exemption (e.g. adding a type guard, or extending to DoWhileStatement) would require updating all three in non-obvious ways.

Merge them into a single helper:

function isDirectTestCondition(parent: estree.Node, expr: estree.AssignmentExpression) {
  return (
    (parent.type === 'IfStatement' ||
      parent.type === 'WhileStatement' ||
      parent.type === 'ForStatement') &&
    parent.test === expr
  );
}

Then replace the three call sites with !isDirectTestCondition(parent, assignment) and drop isIfCondition, isWhileCondition, and isForCondition.

  • Mark as noise

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Merged isIfCondition, isWhileCondition, and isForCondition into a single isDirectTestCondition helper covering IfStatement, WhileStatement, DoWhileStatement, and ForStatement. The three separate call sites are replaced by a single !isDirectTestCondition(parent, assignment).

@sonar-nigel
Copy link
Contributor Author

sonar-nigel bot commented Mar 19, 2026

Acknowledged — this is an automated PR summary. No action needed.

…ules/S1121/rule.ts

Comment: **Logic duplication:** `isIfCondition` and `isForCondition` (line 124) both implement `parent.type === 'XStatement' && parent.test === expr`, which is structurally identical to the existing `isWhileCondition`. All three functions are now parallel — any future behavioral change to the "direct test condition" exemption (e.g. adding a type guard, or extending to `DoWhileStatement`) would require updating all three in non-obvious ways.

Merge them into a single helper:
```typescript
function isDirectTestCondition(parent: estree.Node, expr: estree.AssignmentExpression) {
  return (
    (parent.type === 'IfStatement' ||
      parent.type === 'WhileStatement' ||
      parent.type === 'ForStatement') &&
    parent.test === expr
  );
}
```
Then replace the three call sites with `!isDirectTestCondition(parent, assignment)` and drop `isIfCondition`, `isWhileCondition`, and `isForCondition`.

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

sonar-nigel bot commented Mar 19, 2026

Thank you for the review!

s1121-assignment-in-conditional-expressions-for-idiomatic-patterns-sonnet
@francois-mora-sonarsource
Copy link
Contributor

Minor observation: two test cases were removed from invalid but not added to valid, leaving a small documentation gap.

Before this PR, if (a = b = 0) {} and for (; i = j = 0;); both produced 1 error each. After the fix they produce 0 errors (outer assignment suppressed by isIfCondition/isForCondition, inner chained assignment by the existing isEnclosingChain). Since these are genuinely valid patterns under the new rule, consider adding them to the valid array so the test suite explicitly documents the expected behavior.

@francois-mora-sonarsource
Copy link
Contributor

LGTM - adds three structurally precise guards (isReturnStatement, isIfCondition, isForCondition) to S1121 to suppress false positives for idiomatic assignment patterns in return statements, if-condition slots, and for-test slots — #6643

Comment: Minor observation: two test cases were removed from `invalid` but not added to `valid`, leaving a small documentation gap.

Before this PR, `if (a = b = 0) {}` and `for (; i = j = 0;);` both produced 1 error each. After the fix they produce 0 errors (outer assignment suppressed by `isIfCondition`/`isForCondition`, inner chained assignment by the existing `isEnclosingChain`). Since these are genuinely valid patterns under the new rule, consider adding them to the `valid` array so the test suite explicitly documents the expected behavior.
@sonar-nigel
Copy link
Contributor Author

sonar-nigel bot commented Mar 19, 2026

Thanks for the review and approval!

@sonar-nigel
Copy link
Contributor Author

sonar-nigel bot commented Mar 19, 2026

Good catch. Added both if (a = b = 0) {} and for (; i = j = 0;); to the valid array so the test suite explicitly documents that these are accepted patterns under the new rule.

@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.

Conclusion: Clean, correct implementation. The guard functions are well-scoped, the valid/invalid test boundary is properly drawn, and the ruling snapshot changes are consistent with the stated ~560 suppressed false positives.

🗣️ Give feedback

@sonar-nigel
Copy link
Contributor Author

sonar-nigel bot commented Mar 19, 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