Skip to content

Commit 3e5b988

Browse files
committed
fix(no-unnecessary-condition): use resolved call types for optional chains (#792)
fixes #790
1 parent 1182cea commit 3e5b988

File tree

2 files changed

+78
-3
lines changed

2 files changed

+78
-3
lines changed

internal/rules/no_unnecessary_condition/no_unnecessary_condition.go

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -695,14 +695,30 @@ var NoUnnecessaryConditionRule = rule.Rule{
695695
}
696696
}
697697

698-
// Helper: Get the return type of a call expression's function
699-
// Returns the function's return type, or the full expression type for union of functions
698+
// Helper: Get the effective type of a call expression.
699+
// For plain calls, prefer the resolved call-site type so overloaded APIs like
700+
// react-hook-form's getValues(path) use the selected overload.
701+
// For calls that are themselves part of an optional chain, fall back to the
702+
// callee's return type so we ignore undefined introduced only by short-circuiting
703+
// earlier chain segments.
700704
getCallReturnType := func(callExpr *ast.Node) *checker.Type {
701705
if callExpr == nil || callExpr.Kind != ast.KindCallExpression {
702706
return nil
703707
}
704708

705709
call := callExpr.AsCallExpression()
710+
if call.QuestionDotToken == nil && !hasOptionalChain(call.Expression) {
711+
if callType := getResolvedType(callExpr); callType != nil {
712+
return callType
713+
}
714+
}
715+
716+
if resolvedSignature := checker.Checker_getResolvedSignature(ctx.TypeChecker, callExpr, nil, checker.CheckModeNormal); resolvedSignature != nil {
717+
if returnType := ctx.TypeChecker.GetReturnTypeOfSignature(resolvedSignature); returnType != nil {
718+
return returnType
719+
}
720+
}
721+
706722
funcType := getResolvedType(call.Expression)
707723
if funcType == nil {
708724
return nil
@@ -862,6 +878,34 @@ var NoUnnecessaryConditionRule = rule.Rule{
862878
return ctx.TypeChecker.GetTypeAtLocation(accessExpr)
863879
}
864880

881+
isCallExpressionNullableOriginFromCallee := func(callExpr *ast.CallExpression) bool {
882+
if callExpr == nil {
883+
return false
884+
}
885+
886+
prevType := getResolvedType(callExpr.Expression)
887+
if prevType == nil || !utils.IsUnionType(prevType) {
888+
return false
889+
}
890+
891+
isOwnNullable := false
892+
for _, part := range prevType.Types() {
893+
signatures := ctx.TypeChecker.GetCallSignatures(part)
894+
for _, sig := range signatures {
895+
returnType := ctx.TypeChecker.GetReturnTypeOfSignature(sig)
896+
if returnType != nil && isNullishType(returnType) {
897+
isOwnNullable = true
898+
break
899+
}
900+
}
901+
if isOwnNullable {
902+
break
903+
}
904+
}
905+
906+
return !isOwnNullable && isNullishType(prevType)
907+
}
908+
865909
// checkOptionalChain validates optional chaining (?.) to detect unnecessary usage.
866910
//
867911
// Optional chaining is unnecessary when the expression being accessed is never nullish.
@@ -1294,6 +1338,16 @@ var NoUnnecessaryConditionRule = rule.Rule{
12941338
return
12951339
}
12961340

1341+
if isCallExpr(expression) {
1342+
callExpr := expression.AsCallExpression()
1343+
if isCallExpressionNullableOriginFromCallee(callExpr) {
1344+
exprType = removeNullishFromType(exprType)
1345+
if exprType == nil {
1346+
return
1347+
}
1348+
}
1349+
}
1350+
12971351
// Special case: if expression is a call to a union of functions
12981352
// and any function returns nullish, allow the optional chain
12991353
// e.g., type Foo = (() => undefined) | (() => number) | null

internal/rules/no_unnecessary_condition/no_unnecessary_condition_test.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1127,7 +1127,6 @@ type fn = () => void;
11271127
declare function foo(): void | fn;
11281128
const bar = foo()?.();
11291129
`},
1130-
11311130
// Private fields with exact optional property types
11321131
{
11331132
Code: `
@@ -1303,6 +1302,28 @@ function reproBrandedRecordAssignmentWithNoUncheckedIndexedAccess(key: BrandedSt
13031302
`,
13041303
TSConfig: "tsconfig.noUncheckedIndexedAccess.json",
13051304
},
1305+
{Code: `
1306+
type Item = { action: 'create'; value: number } | null | undefined;
1307+
1308+
declare function getValues(): { data: Item[] };
1309+
declare function getValues(name: ` + "`data.${number}`" + `): Item;
1310+
1311+
const value = getValues(` + "`data.0`" + `)?.value;
1312+
`},
1313+
{Code: `
1314+
type Item = { action: 'create'; value: number } | null | undefined;
1315+
1316+
type Form =
1317+
| {
1318+
getValues(): { data: Item[] };
1319+
getValues(name: ` + "`data.${number}`" + `): Item;
1320+
}
1321+
| undefined;
1322+
1323+
declare const form: Form;
1324+
1325+
const value = form?.getValues(` + "`data.0`" + `)?.value;
1326+
`},
13061327
}, []rule_tester.InvalidTestCase{
13071328
// Basic always truthy/falsy cases
13081329
{

0 commit comments

Comments
 (0)