Skip to content

JS-1255 Fix false positives in S4325 for type assertions narrowing generic/union return types#6464

Open
ss-vibe-bot[bot] wants to merge 10 commits intomasterfrom
fix/JS-1255-fix-fp-on-s4325-type-assertions-narrowing-genericunion-return-types-opus
Open

JS-1255 Fix false positives in S4325 for type assertions narrowing generic/union return types#6464
ss-vibe-bot[bot] wants to merge 10 commits intomasterfrom
fix/JS-1255-fix-fp-on-s4325-type-assertions-narrowing-genericunion-return-types-opus

Conversation

@ss-vibe-bot
Copy link
Contributor

@ss-vibe-bot ss-vibe-bot bot commented Feb 26, 2026

Fixes false positives in the no-unnecessary-type-assertion rule (S4325) where type assertions that genuinely narrow types were incorrectly flagged. The upstream ESLint rule uses reference equality to compare types, which fails when TypeScript infers generic parameters from the assertion context.

Changes

  • Decorator for S4325: Adds a decorator that intercepts rule reports and suppresses false positives in three cases:
    • Type assertions on generic function/method return types
    • Non-null assertions on declared nullable types (T | null, T | undefined)
    • Type assertions to any/unknown when the source type differs
  • Ruling refinements: Corrected ruling analysis for entries that were previously misidentified as false positives — non-null assertions narrowed by TypeScript's flow analysis (assignments, try-finally blocks, null guards) are genuinely unnecessary and correctly flagged
  • Code quality fixes: Merged nested if statements into single conditions (S1066) in decorator.ts — no behavioral changes
  • Test coverage: Added unit tests covering previously untested code paths (non-generic call assertions, non-null assertions on non-union return types, call expressions without type info)

Relates to JS-1255

Vibe Bot and others added 7 commits February 25, 2026 19:41
Tests cover the scenario where type assertions narrowing generic or
union return types are incorrectly flagged as unnecessary. The tests
verify that assertions on generic function/method calls, casts to
any/unknown, and non-null assertions on declared nullable types are
not reported, while truly unnecessary assertions are still flagged.

Relates to JS-1255
Add a decorator to the no-unnecessary-type-assertion rule (S4325) that
suppresses false positives where type assertions genuinely narrow types.
The upstream rule uses reference equality to compare types, which fails
when TypeScript infers generic parameters from the assertion context.

The decorator intercepts reports and suppresses them in three cases:
- Type assertions on generic function/method return types
- Non-null assertions on declared nullable types (T | null, T | undefined)
- Type assertions to any/unknown when the source type differs

Invalid test cases include output fields for the fixable rule's auto-fix
to satisfy ESLint RuleTester requirements. Implementation follows the
approved proposal with an any-to-any guard to preserve true positives.

Relates to JS-1255
Corrected ruling analysis for 5 entries that were incorrectly marked
as false positives in the previous attempt. These non-null assertions
(vuetify scope!, postgraphql result!, courselit course!) are genuinely
unnecessary because TypeScript's flow analysis with strictNullChecks
correctly narrows the types after assignments, try-finally blocks,
and null guards respectively. No implementation changes needed — the
decorator already handles these cases correctly by deferring to the
resolved type when strictNullChecks is enabled.
Ticket: JS-1255

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

Co-Authored-By: Claude <noreply@anthropic.com>
Fixed 3 S1066 (collapsible-if) code smells in decorator.ts by merging
nested if statements into single conditions using logical AND operators.
The three merged statements were: the TSAsExpression/TSTypeAssertion
type check with shouldSuppressTypeAssertion call, the TSNonNullExpression
check with shouldSuppressNonNullAssertion call, and the declaration type
check with typeNodeContainsNullOrUndefined in the for-loop. No behavioral
changes; purely structural improvements to reduce nesting.
Add unit tests for S4325 decorator to cover previously untested code paths:
- Non-generic function call assertions that should still be flagged (isCalleeGeneric returns false)
- Non-null assertions on non-union return types with strictNullChecks
- Non-null assertions on call expressions without type checking (no symbol at location)

This improves code coverage to meet the quality gate threshold.

Co-Authored-By: Claude <noreply@anthropic.com>
Ticket: JS-1255

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

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
Copy link
Contributor

github-actions bot commented Feb 26, 2026

Ruling Report

Code no longer flagged (33 issues)

S4325

TypeScript/src/compiler/factory.ts:511

   509 | 
   510 |     export function updateFunctionTypeNode(node: FunctionTypeNode, typeParameters: NodeArray<TypeParameterDeclaration> | undefined, parameters: NodeArray<ParameterDeclaration>, type: TypeNode | undefined) {
>  511 |         return <FunctionTypeNode>updateSignatureDeclaration(node, typeParameters, parameters, type);
   512 |     }
   513 | 

TypeScript/src/compiler/factory.ts:519

   517 | 
   518 |     export function updateConstructorTypeNode(node: ConstructorTypeNode, typeParameters: NodeArray<TypeParameterDeclaration> | undefined, parameters: NodeArray<ParameterDeclaration>, type: TypeNode | undefined) {
>  519 |         return <ConstructorTypeNode>updateSignatureDeclaration(node, typeParameters, parameters, type);
   520 |     }
   521 | 

TypeScript/src/compiler/parser.ts:678

   676 |             sourceFile.statements = parseList(ParsingContext.SourceElements, parseStatement);
   677 |             Debug.assert(token() === SyntaxKind.EndOfFileToken);
>  678 |             sourceFile.endOfFileToken = <EndOfFileToken>parseTokenNode();
   679 | 
   680 |             setExternalModuleIndicator(sourceFile);

TypeScript/src/compiler/parser.ts:2933

  2931 |             // for cases like > > =  becoming >>=
  2932 |             if (isLeftHandSideExpression(expr) && isAssignmentOperator(reScanGreaterToken())) {
> 2933 |                 return makeBinaryExpression(expr, <BinaryOperatorToken>parseTokenNode(), parseAssignmentExpressionOrHigher());
  2934 |             }
  2935 | 

TypeScript/src/compiler/parser.ts:3368

  3366 |                 }
  3367 |                 else {
> 3368 |                     leftOperand = makeBinaryExpression(leftOperand, <BinaryOperatorToken>parseTokenNode(), parseBinaryExpressionOrHigher(newPrecedence));
  3369 |                 }
  3370 |             }

TypeScript/src/compiler/transformers/es2015.ts:2843

  2841 |             }
  2842 |             else {
> 2843 |                 let clone = <IterationStatement>getMutableClone(node);
  2844 |                 // clean statement part
  2845 |                 clone.statement = undefined;

ag-grid/src/ts/componentProvider.ts:194

   192 |         }
   193 | 
>  194 |         let componentToUse:ComponentToUse<A,B> = <ComponentToUse<A,B>>this.getComponentToUse(holder, componentName, thisComponentConfig, mandatory);
   195 | 
   196 |         if (!componentToUse) return null;

ag-grid/src/ts/componentProvider.ts:204

   202 |         //Using framework component
   203 |         let FrameworkComponentRaw: {new(): B} = componentToUse.component;
>  204 |         return <A>this.frameworkComponentWrapper.wrap(FrameworkComponentRaw, thisComponentConfig.mandatoryMethodList);
   205 |     }
   206 | 

ag-grid/src/ts/componentProvider.ts:208

   206 | 
   207 |     public createAgGridComponent<A extends IComponent<any>> (holder:GridOptions | ColDef | ColGroupDef, componentName:string, defaultComponentName:string, agGridParams:any, mandatory:boolean = true): A{
>  208 |         let component: A = <A>this.newAgGridComponent(holder, componentName, defaultComponentName, mandatory);
   209 |         if (!component) return null;
   210 | 

ag-grid/src/ts/componentProvider.ts:230

   228 | 
   229 |     public newDateComponent (params: IDateParams): IDateComp{
>  230 |         return <IDateComp>this.createAgGridComponent(this.gridOptions, "dateComponent", "dateComponent", params);
   231 |     }
   232 | 

...and 23 more

📋 View full report

Code no longer flagged (33)

S4325

S7728

@ss-vibe-bot
Copy link
Contributor Author

ss-vibe-bot bot commented Feb 26, 2026

The ruling report confirms the fix is working correctly — 30 previously flagged S4325 issues are no longer reported. No regressions (no newly flagged issues). No action needed.

@ss-vibe-bot ss-vibe-bot bot marked this pull request as ready for review February 26, 2026 09:28
@francois-mora-sonarsource
Copy link
Contributor

Team review: remove non-null assertion suppression for projects without strictNullChecks

After reviewing the shouldSuppressNonNullAssertion() logic, we've decided to remove the code path that suppresses non-null assertion reports when strictNullChecks is disabled.

The rationale: without strictNullChecks, TypeScript erases null and undefined from all types, so ! is effectively a no-op — the type is already non-null from the compiler's point of view. The upstream rule is therefore correct to flag it. Suppressing the report here would allow developers to annotate nullable declarations with ! without actually handling null, which defeats the purpose of the rule and hides future issues when the project eventually enables strictNullChecks.

In a JS→TS migration context, it's better to flag these early so developers write proper null guards (if (x !== null)) rather than silently accumulate ! assertions that will need to be revisited later.

Requested change: Remove the else branch (the strictNullChecks: false code path) from shouldSuppressNonNullAssertion() in packages/jsts/src/rules/S4325/decorator.ts. If the strictNullChecks: true branch is the only remaining case, evaluate whether the function is still needed.

If you'd like to measure the impact of this change on real-world projects before removing it, we can create dogfood branches for that purpose.

Comment: Team review: remove non-null assertion suppression for projects without strictNullChecks

After reviewing the shouldSuppressNonNullAssertion() logic, we've decided to remove the code path that suppresses non-null assertion reports when strictNullChecks is disabled.

The rationale: without strictNullChecks, TypeScript erases null and undefined from all types, so ! is effectively a no-op — the type is already non-null from the compiler's point of view. The upstream rule is therefore correct to flag it. Suppressing the report here would allow developers to annotate nullable declarations with ! without actually handling null, which defeats the purpose of the rule and hides future issues when the project eventually enables strictNullChecks.

In a JS→TS migration context, it's better to flag these early so developers write proper null guards (if (x !== null)) rather than silently accumulate ! assertions that will need to be revisited later.

Requested change: Remove the else branch (the strictNullChecks: false code path) from shouldSuppressNonNullAssertion() in packages/jsts/src/rules/S4325/decorator.ts. If the strictNullChecks: true branch is the only remaining case, evaluate whether the function is still needed.

If you'd like to measure the impact of this change on real-world projects before removing it, we can create dogfood branches for that purpose.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@ss-vibe-bot
Copy link
Contributor Author

ss-vibe-bot bot commented Feb 26, 2026

Done. Removed the strictNullChecks: false else branch from shouldSuppressNonNullAssertion() in decorator.ts. The function now only suppresses when strictNullChecks (or strict) is enabled and the resolved type still contains null/undefined after flow narrowing — which is the correct, conservative behavior.

The typeNodeContainsNullOrUndefined() helper became dead code with that removal and was also deleted.

Test updates:

  • The two "should not flag non-null assertions on declared nullable types" cases (property Api | null, variable T | undefined) are now invalid with the expected auto-fix output, reflecting that without strictNullChecks these assertions are correctly flagged.
  • The "non-null on call expression without type checking" test case (added to cover the removed else-branch path) was removed as it no longer exercises any distinct code path.

github-actions bot and others added 2 commits February 26, 2026 11:45
🤖 Generated with GitHub Actions
Add unit tests to cover previously untested code paths in the S4325
decorator.ts rule implementation:

- Test that reports without a 'node' property are forwarded as-is
  (defensive check at lines 49-50)
- Test that reports are forwarded when TypeScript parser services lack
  a program (lines 55-56)
- Test suppression of contextually unnecessary non-null assertions on
  nullable union types with strictNullChecks enabled (line 69)

This improves code coverage to meet the quality gate threshold.

Co-Authored-By: Claude <noreply@anthropic.com>
@sonarqube-next
Copy link

@francois-mora-sonarsource
Copy link
Contributor

LGTM

@francois-mora-sonarsource francois-mora-sonarsource requested a review from a team February 26, 2026 13:48
@ss-vibe-bot
Copy link
Contributor Author

ss-vibe-bot bot commented Feb 26, 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