Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions internal/rule_tester/__snapshots__/no-unnecessary-condition.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1961,3 +1961,19 @@ Message: Unnecessary conditional, value is always truthy.
| ~~~~
3 |
---

[TestNoUnnecessaryConditionRule/invalid-142 - 1]
Diagnostic 1: neverOptionalChain (6:11 - 6:35)
Message: Unnecessary optional chain on a non-nullish value.
5 | declare const b: Box;
6 | const r = b.get('a')?.get('b')?.get('c');
| ~~~~~~~~~~~~~~~~~~~~~~~~~
7 |

Diagnostic 2: neverOptionalChain (6:11 - 6:25)
Message: Unnecessary optional chain on a non-nullish value.
5 | declare const b: Box;
6 | const r = b.get('a')?.get('b')?.get('c');
| ~~~~~~~~~~~~~~~
7 |
---
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,34 @@ var NoUnnecessaryConditionRule = rule.Rule{
return constraintType, false
}

// constrainConditionalType returns the base constraint of a deferred
// conditional (the union of its branches). An unresolved conditional has no
// nullish flag, so isNullishType misreads it.
constrainConditionalType := func(t *checker.Type) *checker.Type {
if t == nil || checker.Type_flags(t)&checker.TypeFlagsConditional == 0 {
return t
}
if constraint := checker.Checker_getBaseConstraintOfType(ctx.TypeChecker, t); constraint != nil {
return constraint
}
return t
}

// isNullishType, but applies constrainConditionalType at every level,
// recursing into union members.
var isConstrainedNullishType func(t *checker.Type) bool
isConstrainedNullishType = func(t *checker.Type) bool {
if t == nil {
return false
}
t = constrainConditionalType(t)
if utils.IsUnionType(t) {
return slices.ContainsFunc(t.Types(), isConstrainedNullishType)
}
flags := checker.Type_flags(t)
return flags&(checker.TypeFlagsNull|checker.TypeFlagsUndefined|checker.TypeFlagsVoid) != 0
}

getPropertyNameFromLiteralType := func(t *checker.Type) (string, bool) {
if t == nil {
return "", false
Expand Down Expand Up @@ -717,8 +745,9 @@ var NoUnnecessaryConditionRule = rule.Rule{
for _, part := range prevType.Types() {
signatures := ctx.TypeChecker.GetCallSignatures(part)
for _, sig := range signatures {
// Constrain so a conditional nullish return isn't misread as non-nullable.
returnType := ctx.TypeChecker.GetReturnTypeOfSignature(sig)
if returnType != nil && isNullishType(returnType) {
if returnType != nil && isConstrainedNullishType(returnType) {
isOwnNullable = true
break
}
Expand Down Expand Up @@ -1264,7 +1293,7 @@ var NoUnnecessaryConditionRule = rule.Rule{
}
}

if !isNullishType(exprType) {
if !isConstrainedNullishType(exprType) {
ctx.ReportNode(node, buildNeverOptionalChainMessage())
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,57 @@ assertSecond(...[], true);
if (typeof value === 'object' && value !== null) { console.log('foo'); }
}`,
},
// Deferred conditional with a nullish branch, left unresolved on chains of
// length >= 3. The `?.` links are required, so none of these is reported.
{Code: `
interface Box {
get<H = never>(name: string): [H] extends [never] ? Box | null : Box;
}
declare const b: Box;
const r = b.get('a')?.get('b')?.get('c');
`},
{Code: `
interface BoxUndef {
get<H = never>(name: string): [H] extends [never] ? BoxUndef | undefined : BoxUndef;
}
declare const b: BoxUndef;
const r = b.get('a')?.get('b')?.get('c');
`},
{Code: `
interface BoxExplicit {
get<H>(name: string): [H] extends [never] ? BoxExplicit | null : BoxExplicit;
}
declare const b: BoxExplicit;
const r = b.get<never>('a')?.get<never>('b')?.get<never>('c');
`},
{Code: `
type Ret<H> = [H] extends [never] ? BoxAlias | null : BoxAlias;
interface BoxAlias {
get<H = never>(name: string): Ret<H>;
}
declare const b: BoxAlias;
const r = b.get('a')?.get('b')?.get('c');
`},
// Deferred conditional nested as a union member (`X | (cond)`). The
// nullish branch must still be seen through the union.
{Code: `
interface BoxUnion {
get<H = never>(name: string): BoxUnion | ([H] extends [never] ? null : BoxUnion);
}
declare const b: BoxUnion;
const r = b.get('a')?.get('b')?.get('c');
`},
// Deferred conditional reached through property access in a generic scope,
// where the type parameter keeps it unresolved. Exercises the nullish check
// in checkOptionalChain rather than the call-expression path.
{Code: `
interface BoxGeneric<H> {
next: [H] extends [never] ? BoxGeneric<H> | null : BoxGeneric<H>;
}
function foo<T>(b: BoxGeneric<T>): unknown {
return b.next?.next?.next;
}
`},
}, []rule_tester.InvalidTestCase{
// Basic always truthy/falsy cases
{
Expand Down Expand Up @@ -2853,5 +2904,21 @@ if (true || false) {}
{MessageId: "alwaysTruthy"},
},
},
// Precision guard: both branches non-nullish, so the `?.` links are
// unnecessary and must be reported. Appended last to keep index-keyed
// snapshots aligned.
{
Code: `
interface Box {
get<H = never>(name: string): [H] extends [never] ? Box : Box;
}
declare const b: Box;
const r = b.get('a')?.get('b')?.get('c');
`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "neverOptionalChain"},
{MessageId: "neverOptionalChain"},
},
},
})
}