Skip to content

JS-1462 Fix FP on S6582 when optional chaining would introduce unsafe undefined#6637

Open
sonar-nigel[bot] wants to merge 40 commits intomasterfrom
fix/JS-1462-fix-fp-on-s6582-multi-condition-logic-with--and--cannot-use-optional-chaining-sonnet
Open

JS-1462 Fix FP on S6582 when optional chaining would introduce unsafe undefined#6637
sonar-nigel[bot] wants to merge 40 commits intomasterfrom
fix/JS-1462-fix-fp-on-s6582-multi-condition-logic-with--and--cannot-use-optional-chaining-sonnet

Conversation

@sonar-nigel
Copy link
Copy Markdown
Contributor

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

Rule Profile

Field Value
Rule S6582 — Optional chaining should be preferred
Severity / type Minor SUGGESTION
Sonar Way Yes
Scope Main

Genuine violation example:

// Reports: prefer `user?.name` over `user && user.name`
function getDisplayName(user: User | null): string | undefined {
  return user && user.name;
}

False Positive Pattern

The upstream prefer-optional-chain rule suggests replacing a && a.prop (or its negated form !a || !a.prop) with a?.prop without consulting the surrounding type context. When the expression is used in a typed assignment or return position that declares T | null (not T | undefined), the replacement changes the result type from T | null to T | undefined, breaking type assignability. Because the upstream rule does not call checker.getContextualType(), it cannot distinguish safe rewrites (boolean/void contexts) from unsafe ones (narrowly typed assignments or returns).

Pattern 1 — typed variable declaration (T | null):

// FP: replacing with `arr?.length` yields `number | undefined`, not `number | null`
const len: number | null = arr && arr.length;

Pattern 2 — typed return value (ISnapshot | null):

function getSnapshot(file: File | null): ISnapshot | null {
  return file && file.snapshot;  // a?.snapshot would return `ISnapshot | undefined`
}

Pattern 3 — negated double-null guard in typed context:

function concatenate<T>(array1: T[] | null, array2: T[] | null): T[] | null {
  if (!array1 || !array1.length) return array2;  // FP before fix
  // …
}

False Negative Risk

The fix suppresses reports only when the contextual type explicitly excludes undefined. Three classes of patterns always pass through unconditionally:

  1. No contextual type (e.g., if (a && a.prop), while (a && a.prop)) — boolean/void position imposes no contextual type, so the report is always emitted.
  2. Comparison RHS (e.g., a && a.prop !== b) — a comparison operator returns boolean regardless of optional chaining; undefined never leaks.
  3. Double-negation guard (e.g., !a || !a.prop) — ! always returns boolean; !a?.prop is semantically identical.

Accepted known false negative: function f(arr: string[] | null): boolean { return !arr || !arr.length; }. The ! operator makes the replacement type-safe, but the negation-guard bypass fires before the contextual-type check, so the report is emitted correctly. However, if a negation pattern appears in a function explicitly typed as returning boolean, the contextual-type check would normally suppress it. The negation bypass pre-empts this, so no false negative arises there either.

Conservative accepted false negative: a && a.p == null patterns. The upstream rule does not report these (the loose == null rewrite changes semantics when a is null), so the interceptor never sees them; no regression from this fix.

Genuine violation that passes through (not suppressed):

// No contextual type — report is correctly emitted
if (user && user.name) {
  console.log(user.name);
}

The risk of over-suppression is bounded: suppression only fires when strictNullChecks is enabled, a contextual type exists, and none of its union constituents include Undefined, Any, Unknown, or Void.


Implementation

How the rule works today: S6582 wraps the upstream @typescript-eslint/prefer-optional-chain rule. Before this fix, it passed reports through unchanged after a parser-services guard, offering no protection against type-unsafe suggestions in strictly-typed assignment contexts.

What changed:

1. strictNullChecks gate — skip the interceptor entirely when strictNullChecks is off; without it, undefined is implicitly assignable to every type and contextual-type reasoning is meaningless:

if (!services.program.getCompilerOptions().strictNullChecks) {
  return preferOptionalChainRule.create(context);
}

2. findReportNode helper — the upstream rule may report via loc instead of node. This helper resolves the ESTree LogicalExpression spanning the reported range by walking up from getNodeByRangeIndex, and falls back to ctx.report(descriptor) on any missing information so it is never silently lossy:

function findReportNode(ctx, descriptor): (Rule.Node & { range }) | null {
  if ('node' in descriptor) return descriptor.node;
  // walk up from startIndex until node.range[1] >= endIndex
}

3. Negation-pattern bypass!a || !a.prop always stays boolean, allow unconditionally. Both sides must be negated; !a || a.prop !== b falls through to the contextual-type check:

function isNegatedOptionalChainGuard(node) {
  return node.operator === '||'
    && node.left.operator === '!'
    && node.right.operator === '!';
}

4. Comparison-pattern bypassa && a.prop !== b: comparison operators (==, !=, ===, !==, <, >, <=, >=, instanceof, in) always yield boolean; allow unconditionally:

if (node.right.type === 'BinaryExpression' &&
    ['==', '!=', '===', '!==', '<', '>', '<=', '>=', 'instanceof', 'in']
      .includes(node.right.operator)) {
  ctx.report(descriptor); return;
}

5. allowsUndefined contextual-type check — suppress only when contextual type exists and none of its union constituents include Undefined | Any | Unknown | Void:

function allowsUndefined(type: ts.Type): boolean {
  const constituents = type.isUnion() ? type.types : [type];
  return constituents.some(t =>
    (t.flags & ts.TypeFlags.Undefined) !== 0 ||
    (t.flags & ts.TypeFlags.Any) !== 0 ||
    (t.flags & ts.TypeFlags.Unknown) !== 0 ||
    (t.flags & ts.TypeFlags.Void) !== 0,
  );
}

Technical Summary

Files changed:

  • packages/analysis/src/jsts/rules/S6582/rule.ts — core implementation: added interceptReport wrapper with negation/comparison bypasses, contextual-type suppression, strictNullChecks gate, findReportNode helper, and allowsUndefined helper (~+165 lines)
  • packages/analysis/src/jsts/rules/S6582/unit.test.ts — unit tests: upstream sentinel, defensive fallback tests, and main valid/invalid test suite covering all bypass paths and suppression cases (~+277 lines)
  • packages/analysis/src/jsts/rules/S6582/fixtures/tsconfig.json — tsconfig for typed test fixtures enabling strictNullChecks (+5 lines)
  • packages/analysis/src/jsts/rules/S6582/fixtures/index.ts — typed fixture file for unit tests (+1 line)
  • packages/analysis/src/jsts/rules/S6582/fixtures/no-strict-null-checks/index.ts — fixture for no-strict-null-checks test path (+1 line)
  • packages/analysis/src/jsts/rules/S6582/fixtures/no-strict-null-checks/tsconfig.json — tsconfig without strictNullChecks for no-strict path tests (+3 lines)
  • its/ruling/src/test/expected/TypeScript/javascript-S6582.json — ruling baseline: two new true positives added for a && a.prop === value patterns in TypeScript compiler source (-1 net line)
  • its/ruling/src/test/expected/paper.js/javascript-S6582.json — ruling baseline updated for paper.js project (-/+ 4 lines)

Net change: +447 insertions, −10 deletions across 8 files.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 19, 2026

Ruling Report

Code no longer flagged (3 issues)

S6582

TypeScript/lib/tsc.js:44761

  44759 |             table.forEach(function (labelMarker, labelText) {
  44760 |                 var statements = [];
> 44761 |                 if (!outerLoop || (outerLoop.labels && outerLoop.labels.get(labelText))) {
  44762 |                     var label = ts.createIdentifier(labelText);
  44763 |                     statements.push(isBreak ? ts.createBreak(label) : ts.createContinue(label));

paper.js/src/event/Key.js:145

   143 |             // Chrome doesn't fire keypress events for command and alt keys,
   144 |             // so we need to handle this in a way that works across all OSes.
>  145 |             if (key.length > 1 || agent && (agent.chrome && (event.altKey
   146 |                         || agent.mac && event.metaKey
   147 |                         || !agent.mac && event.ctrlKey))) {

paper.js/src/path/PathItem.Boolean.js:442

   440 |                     // This is a new curve, update clearHandles setting.
   441 |                     clearHandles = !curve.hasHandles()
>  442 |                             || clearLookup && clearLookup[getId(curve)];
   443 |                     // Keep track of locations for later curve-time
   444 |                     // renormalization within the curve.

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

sonar-review-alpha bot commented Mar 20, 2026

Summary

⚠️ The PR description exceeded the analysis limit and was truncated. The review may not reflect all context.

Adds a type-safety interceptor to rule S6582 to eliminate false positives where optional chaining rewrites would introduce unsound undefined types.

The Problem: S6582 wraps TypeScript ESLint's prefer-optional-chain rule, which suggests replacing a && a.prop with a?.prop. However, optional chaining returns T | undefined when a is nullish, not T | null. If the surrounding context (a typed return, variable assignment, or function call) expects T | null and excludes undefined, the rewrite silently breaks type safety under strictNullChecks.

The Solution: Before reporting, the interceptor uses TypeScript's type checker to:

  1. Skip entirely if strictNullChecks is disabled (null/undefined reasoning is meaningless without it)
  2. Detect negation patterns (!a || !a.prop) and comparison patterns (a && a.prop !== b), which always return boolean and are safe to rewrite
  3. Check the contextual type of the expression — if it excludes undefined (and isn't any/unknown/void), suppress the report

The fix includes comprehensive tests verifying that upstream reports still fire in boolean contexts, that the interceptor's defensive fallbacks work, and that genuine violations still pass through.

What reviewers should know

Start here:

  • Read the interceptor implementation in rule.ts lines 47–211 — the helper functions are well-documented and the flow is logical: gate on strictNullChecks → match safe patterns → check contextual type
  • Review the unit test structure in unit.test.ts: upstream sentinel tests confirm the rule still catches violations, then organized test groups show false-positive suppression cases

Key design decisions:

  • Conservative approach: Negation patterns like !a || !a.prop in boolean-typed contexts are suppressed (known false negative), because !a?.prop is also boolean and type-safe, but the implementation treats all negation patterns uniformly for simplicity
  • Comparison bypass covers most operators (===, !==, <, >, etc.) but includes loose equality (==, !=), which is noted as acceptable
  • strictNullChecks gate is critical — without it, the entire interceptor is skipped to maintain correct behavior for untyped projects

What to look for:

  • The findReportNode function gracefully handles both node and loc descriptors, with fallbacks to ctx.report() if lookup fails
  • Type inspection in allowsUndefined() correctly checks union constituents for undefined/any/unknown/void flags
  • Ruling test expectations (TypeScript and paper.js) show realistic reduction in false positives (~2–3 lines per file)

  • Generate Walkthrough
  • Generate Diagram

🗣️ Give feedback

sonar-review-alpha[bot]

This comment was marked as resolved.

@sonar-nigel
Copy link
Copy Markdown
Contributor Author

sonar-nigel bot commented Mar 20, 2026

Thanks for the summary — this accurately describes the implementation and its intent. No changes needed.

@sonar-nigel
Copy link
Copy Markdown
Contributor Author

sonar-nigel bot commented Mar 20, 2026

Thank you for the thorough review! The latent crash path has been addressed — the guard is now if (!loc || !('start' in loc)), which safely handles a Position (no start/end) by falling through to ctx.report(descriptor) instead of passing undefined to getIndexFromLoc.

sonar-review-alpha[bot]

This comment was marked as resolved.

@sonar-nigel
Copy link
Copy Markdown
Contributor Author

sonar-nigel bot commented Mar 20, 2026

Thank you for the review!

sonar-review-alpha[bot]

This comment was marked as resolved.

@sonar-nigel
Copy link
Copy Markdown
Contributor Author

sonar-nigel bot commented Mar 24, 2026

Thanks for the LGTM! No changes needed.

sonar-review-alpha[bot]

This comment was marked as resolved.

@sonar-nigel
Copy link
Copy Markdown
Contributor Author

sonar-nigel bot commented Mar 25, 2026

Thank you for the review!

@sonar-nigel
Copy link
Copy Markdown
Contributor Author

sonar-nigel bot commented Mar 25, 2026

The latent crash path has been closed — the !('start' in loc) guard was added as suggested in the inline comment, and that thread is resolved. No further changes needed for this summary.

sonar-review-alpha[bot]

This comment was marked as outdated.

sonar-review-alpha[bot]

This comment was marked as outdated.

sonar-review-alpha[bot]

This comment was marked as outdated.

francois-mora-sonarsource pushed a commit that referenced this pull request Mar 27, 2026
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
sonar-review-alpha[bot]

This comment was marked as outdated.

@francois-mora-sonarsource francois-mora-sonarsource removed the request for review from a team March 30, 2026 09:10
francois-mora-sonarsource pushed a commit that referenced this pull request Mar 30, 2026
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
sonar-review-alpha[bot]

This comment was marked as outdated.

sonar-review-alpha[bot]

This comment was marked as resolved.

francois-mora-sonarsource pushed a commit that referenced this pull request Mar 31, 2026
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
sonar-review-alpha[bot]

This comment was marked as outdated.

@sonar-nigel
Copy link
Copy Markdown
Contributor Author

sonar-nigel bot commented Mar 31, 2026

Negation patterns: Good catch — this was not intentional. Added a guard before the contextual type check that detects negation patterns (!a || !a.prop) by checking whether the LogicalExpression has || operator and the left side is a UnaryExpression with !. Since ! always returns boolean regardless of what optional chaining introduces, the report is let through unconditionally for these patterns. Added a test case for function f(arr: string[] | null): boolean { return !arr || !arr.length; } in the invalid section.

Unrelated ruling file: The typescript-S7727.json file has been removed from this PR (addressed in the inline comment on that file).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@francois-mora-sonarsource francois-mora-sonarsource force-pushed the fix/JS-1462-fix-fp-on-s6582-multi-condition-logic-with--and--cannot-use-optional-chaining-sonnet branch from 9cce503 to 4aaf440 Compare April 1, 2026 15:15
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
sonar-review-alpha[bot]

This comment was marked as outdated.

Maven ITS reports line 768 as a valid S6582 issue. The Node.js bridge
virtual TypeScript program infers a more specific type for the untyped
lowercase() parameter, causing the fix to suppress it, but the Maven ITS
(which is the authoritative ruling environment) still reports it.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
sonar-review-alpha[bot]

This comment was marked as outdated.

@francois-mora-sonarsource
Copy link
Copy Markdown
Contributor

Note: fix should be gated on strictNullChecks

The suppression logic in this fix only makes semantic sense when strictNullChecks: true is enabled in the TypeScript compiler options.

Without strictNullChecks, TypeScript does not differentiate null/undefined from other types — undefined is implicitly assignable to string, number, etc. So the concern "optional chaining would introduce undefined where it's not allowed" does not apply: there is no type safety issue to protect against.

The current implementation checks TypeFlags.Undefined on the contextual type, but when strictNullChecks is off, TypeScript still computes a contextual type of e.g. string for an untyped JS function parameter (inferred from call sites when noImplicitAny: true), yet undefined is silently assignable to it. This causes the fix to suppress legitimate issues in JS projects analyzed without strictNullChecks.

Proposed improvement: gate the entire suppression on strictNullChecks being enabled:

const compilerOptions = services.program.getCompilerOptions();
if (!compilerOptions.strictNullChecks) {
  ctx.report(descriptor);
  return;
}

This would make the fix a no-op in JS projects analyzed with the default virtual TypeScript program (which does not set strictNullChecks), consistently with the Maven ITS behavior.

Without strictNullChecks, TypeScript treats null and undefined as
implicit subtypes of every type. Any contextual-type reasoning about
whether undefined is safe to introduce is meaningless in that mode,
since undefined is silently assignable to string, number, etc.

The fix now skips the suppression interceptor entirely when
strictNullChecks is disabled, falling through to the upstream
prefer-optional-chain rule instead. This makes the rule consistent
across both the Node.js bridge ruling and the Maven ITS, which uses
a virtual TypeScript program that does not set strictNullChecks for
plain JavaScript projects.

Ruling expected files updated to reflect new correctly-reported issues
in projects without strictNullChecks (JS and TS alike).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@francois-mora-sonarsource
Copy link
Copy Markdown
Contributor

Review findings from current head (7a5a444):

  1. The new contextual-type suppression is too broad and introduces a false negative for boolean-valued comparison shapes.

In [rule.ts], any reported chain whose contextual type excludes undefined is suppressed. That works for T | null return/assignment contexts, but it also suppresses boolean expressions such as:

return !a || a.module !== b.module;

The optional-chain rewrite here is return a?.module !== b.module;, which still type-checks under strictNullChecks because the comparison result is boolean, not number | undefined/string | undefined.

The current tests lock in that false negative by marking this case as valid in [unit.test.ts].

Please narrow the suppression so it only covers cases where the optional-chain rewrite would actually leak undefined into the surrounding type, and add this boolean-comparison case as an invalid regression test.

  1. The PR metadata is stale and the diff still contains an unrelated README.md change.

The PR body still says the branch updates ~34 files and ~28 ruling baselines, but the current diff is 9 files with only 2 ruling baselines. It would be good to refresh the description before the next review pass, and the README.md rule-count change should be dropped from this FP fix unless it is intentional and explained.

sonar-review-alpha[bot]

This comment was marked as outdated.

Boolean comparison expressions (e.g. a.prop !== b) always return boolean
regardless of optional chaining, so the optional-chain rewrite never leaks
undefined into the surrounding type. The previous contextual-type check
suppressed !a || a.prop !== b because the contextual type is boolean (which
excludes undefined), but the rewrite a?.prop !== b is also boolean — no
type safety issue exists.

Add a new guard: when the right side of the LogicalExpression is a
BinaryExpression with a comparison operator, allow the report unconditionally
and skip the contextual-type suppression.

Move the !a || a.module !== b.module test case from valid to invalid (the
rule should now report it), and add the expected autofix output.

Also revert the unrelated README.md rule-count change.

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

sonar-nigel bot commented Apr 2, 2026

Both points addressed:

1. Narrowed the suppression for boolean-comparison patterns

Added a new guard in rule.ts: when the right-hand side of the reported LogicalExpression is a BinaryExpression with a comparison operator (===, !==, <, >, <=, >=, instanceof, in), the report is allowed unconditionally. For these patterns (e.g. !a || a.module !== b.module), the optional-chain rewrite produces a comparison expression (a?.module !== b.module) whose result is always boolean — no undefined leaks into the surrounding type regardless of context.

The !a || a.module !== b.module test case has been moved from valid to invalid, with the expected autofix output a?.module !== b.module.

2. Dropped the unrelated README.md change

The rule-count update (479→480, 29→30) has been reverted. That change is unrelated to this FP fix and belonged in a separate PR.

PR description refresh (file count / ruling baseline count) is a manual step — the numbers in the description are now stale from earlier iterations but the code changes are limited to 3 files (rule.ts, unit.test.ts, README.md revert) with 2 ruling baselines.

Vibe Bot added 2 commits April 2, 2026 14:49
…6582/rule.ts

Comment: would be nice to check if node is present tho, in case the upstream rule changes at some point. so if node is there we pick it up, otherwise we fallback to this logic. which btw could be added to a helper `getNodeFromLocation`. Probably there is something already in the helpers, not sure
Comment: LGTM! ✅

\n\nSeaworthy fix overall, and the upstream sentinel test is a particularly clever piece of navigation — it'll warn the crew if the upstream rule ever stops raising on these patterns, making the interceptor removable without anyone running aground. One type-safety gap worth examining before you raise anchor.\n\nThe `undefinedAssignable` check in `rule.ts:123–128` tests for `Undefined`, `Any`, and `Unknown` — but not for `ts.TypeFlags.Void`. TypeScript's `void` type is contextually permissive for callback assignments: when you write `const fn: () => void = () => arr && arr.length`, the contextual type of `arr && arr.length` is `void`. Because `void` carries neither the `Undefined` nor `Any` nor `Unknown` flag, `undefinedAssignable` comes back false and the report is suppressed — but `arr?.length` is perfectly assignable to `void`, so the suggestion would have been type-safe. Adding `(t.flags & ts.TypeFlags.Void) !== 0` to the `constituents.some(...)` check would close this narrow false-negative. Worth a conscious decision either way — the PR description doesn't address it, savvy?"

🗣️ [Give feedback](https://sonarsource.enterprise.slack.com/archives/C0AJXQM9MPZ)
@sonar-nigel
Copy link
Copy Markdown
Contributor Author

sonar-nigel bot commented Apr 2, 2026

Good catch — added (t.flags & ts.TypeFlags.Void) !== 0 to the undefinedAssignable check. Since undefined is assignable to void in TypeScript, a void contextual type should not suppress the report. Also added a test case: const fn: () => void = () => arr && arr.length is now correctly flagged as invalid.

sonar-review-alpha[bot]

This comment was marked as outdated.

Vibe Bot and others added 2 commits April 2, 2026 15:33
No implementation changes required. The contextual-type interceptor and
comparison-pattern bypass are correct as-is.

Two ruling entries in TypeScript:lib/tsc.js (lines 50184 and 50201)
were flagged by the rule but missing from the expected baseline. Both
follow the `a && a.prop === value` comparison pattern (specifically
`node.parent.parent && node.parent.parent.kind === 163`), which is a
valid optional-chain candidate identical in structure to other already-
accepted patterns in the same file (e.g., lines 8599 and 37690). The
expected ruling file for the TypeScript project is updated to include
these two lines.

Added one test case inspired by the ruling: `a && a.prop === b`
(comparison pattern with `&&`) to complement the existing
`!a || a.prop !== b` test. Both demonstrate that the comparison-pattern
check correctly allows reports where the right-hand side is a comparison
expression.
@francois-mora-sonarsource
Copy link
Copy Markdown
Contributor

I found one regression in the new comparison bypass.

The bypass added in packages/analysis/src/jsts/rules/S6582/rule.ts only forwards strict/relational comparison operators:

['===', '!==', '<', '>', '<=', '>=', 'instanceof', 'in']

But the upstream prefer-optional-chain rule also reports binary comparison chains for == and != (see @typescript-eslint/eslint-plugin/dist/rules/prefer-optional-chain-utils/gatherLogicalOperands.js, which maps both operators to OperandValidity.Last). Those comparisons are also always boolean after the rewrite, so they need the same bypass.

Today they fall through to the contextual-type suppression and become false negatives in typed-boolean contexts. I confirmed it with a one-off RuleTester run on the current branch:

interface Item { p: string }
function f(a: Item | null): boolean {
  return a && a.p != null;
}

Expected: 1 issue (a?.p != null is still a safe boolean rewrite)
Actual on this branch: 0 issues

Please extend the comparison bypass to cover == / != as well, and add regression tests next to the existing === / !== comparison cases.

Extract node-resolution logic from the anonymous interceptReport callback
into a named helper `findReportNode`, splitting the complexity across two
focused functions each well within the threshold.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Comment: I found one regression in the new comparison bypass.

The bypass added in `packages/analysis/src/jsts/rules/S6582/rule.ts` only forwards strict/relational comparison operators:

```ts
['===', '!==', '<', '>', '<=', '>=', 'instanceof', 'in']
```

But the upstream `prefer-optional-chain` rule also reports binary comparison chains for `==` and `!=` (see `@typescript-eslint/eslint-plugin/dist/rules/prefer-optional-chain-utils/gatherLogicalOperands.js`, which maps both operators to `OperandValidity.Last`). Those comparisons are also always boolean after the rewrite, so they need the same bypass.

Today they fall through to the contextual-type suppression and become false negatives in typed-boolean contexts. I confirmed it with a one-off `RuleTester` run on the current branch:

```ts
interface Item { p: string }
function f(a: Item | null): boolean {
  return a && a.p != null;
}
```

Expected: 1 issue (`a?.p != null` is still a safe boolean rewrite)
Actual on this branch: 0 issues

Please extend the comparison bypass to cover `==` / `!=` as well, and add regression tests next to the existing `===` / `!==` comparison cases.
The upstream prefer-optional-chain rule does not raise on `a && a.p == null`
patterns because the rewrite to `a?.p == null` changes semantics: when `a` is
null, the original short-circuits to null (falsy), while `a?.p == null` evaluates
to `undefined == null` which is `true` (truthy). The upstream rule correctly
avoids suggesting this unsafe transformation, so S6582 receives no report to
intercept, producing 0 errors instead of the expected 1.
@sonarqube-next
Copy link
Copy Markdown

sonarqube-next bot commented Apr 2, 2026

Copy link
Copy Markdown

@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

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