Skip to content

Commit 1052b09

Browse files
committed
fix(prefer-optional-chain): false positive on tricky condition (#831)
fixes #829
1 parent adfd2b7 commit 1052b09

2 files changed

Lines changed: 92 additions & 48 deletions

File tree

internal/rules/prefer_optional_chain/prefer_optional_chain.go

Lines changed: 79 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,42 @@ func (t *TypeInfo) HasNoNullableTypes() bool {
617617
return !t.hasNull && !t.hasUndefined && !t.hasAny && !t.hasUnknown
618618
}
619619

620+
func (t *TypeInfo) CanBeUndefinedLike() bool {
621+
return len(t.parts) == 0 || t.hasUndefined || t.hasVoid || t.hasAny || t.hasUnknown
622+
}
623+
624+
func (t *TypeInfo) CanBeNullishLike() bool {
625+
return len(t.parts) == 0 || t.hasNull || t.hasUndefined || t.hasVoid || t.hasAny || t.hasUnknown
626+
}
627+
628+
func (t *TypeInfo) IsAlwaysUndefinedLike() bool {
629+
if len(t.parts) == 0 || t.IsAnyOrUnknown() {
630+
return false
631+
}
632+
633+
for _, part := range t.parts {
634+
if !utils.IsTypeUndefinedType(part) && !utils.IsTypeVoidType(part) {
635+
return false
636+
}
637+
}
638+
639+
return true
640+
}
641+
642+
func (t *TypeInfo) IsAlwaysNullishLike() bool {
643+
if len(t.parts) == 0 || t.IsAnyOrUnknown() {
644+
return false
645+
}
646+
647+
for _, part := range t.parts {
648+
if !utils.IsTypeNullType(part) && !utils.IsTypeUndefinedType(part) && !utils.IsTypeVoidType(part) {
649+
return false
650+
}
651+
}
652+
653+
return true
654+
}
655+
620656
type chainProcessor struct {
621657
ctx rule.RuleContext
622658
opts PreferOptionalChainOptions
@@ -2302,63 +2338,58 @@ func (processor *chainProcessor) isUnsafeTrailingComparison(chain []Operand, las
23022338
if ast.IsBinaryExpression(unwrappedNode) {
23032339
binExpr := unwrappedNode.AsBinaryExpression()
23042340
op := binExpr.OperatorToken.Kind
2341+
left := ast.SkipParentheses(binExpr.Left)
2342+
right := ast.SkipParentheses(binExpr.Right)
23052343

23062344
var value *ast.Node
2307-
if utils.IsAccessExpression(binExpr.Left) {
2308-
value = binExpr.Right
2309-
} else if utils.IsAccessExpression(binExpr.Right) {
2310-
value = binExpr.Left
2311-
}
2312-
2313-
if value != nil {
2314-
isNull := utils.IsNullLiteral(value)
2315-
isUndefined := utils.IsUndefinedLiteral(value)
2316-
isNullish := isNull || isUndefined
2317-
isLiteral := value.Kind == ast.KindNumericLiteral ||
2318-
value.Kind == ast.KindStringLiteral ||
2319-
value.Kind == ast.KindTrueKeyword ||
2320-
value.Kind == ast.KindFalseKeyword ||
2321-
value.Kind == ast.KindObjectLiteralExpression ||
2322-
value.Kind == ast.KindArrayLiteralExpression
2323-
isUndeclaredVar := ast.IsIdentifier(value) && !isUndefined && !isLiteral
2324-
2325-
unsafe := false
2326-
switch op {
2327-
case ast.KindEqualsEqualsToken:
2328-
if isNullish || isUndeclaredVar {
2329-
unsafe = true
2330-
}
2331-
case ast.KindEqualsEqualsEqualsToken:
2332-
if isUndefined || isUndeclaredVar {
2333-
unsafe = true
2334-
}
2335-
case ast.KindExclamationEqualsToken:
2336-
if !isNullish {
2337-
unsafe = true
2338-
}
2339-
case ast.KindExclamationEqualsEqualsToken:
2340-
if !isUndefined {
2341-
unsafe = true
2342-
}
2343-
// Relational operators are unsafe because undefined/null comparisons
2344-
// produce unexpected results
2345-
case ast.KindLessThanToken,
2346-
ast.KindGreaterThanToken,
2347-
ast.KindLessThanEqualsToken,
2348-
ast.KindGreaterThanEqualsToken:
2349-
unsafe = true
2350-
}
2351-
2352-
if unsafe && !processor.opts.AllowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing {
2353-
return true
2354-
}
2345+
switch {
2346+
case lastOp.comparedExpr != nil && areNodesStructurallyEqual(lastOp.comparedExpr, left):
2347+
value = right
2348+
case lastOp.comparedExpr != nil && areNodesStructurallyEqual(lastOp.comparedExpr, right):
2349+
value = left
2350+
case utils.IsAccessExpression(left):
2351+
value = right
2352+
case utils.IsAccessExpression(right):
2353+
value = left
2354+
}
2355+
2356+
if value != nil &&
2357+
!processor.isSafeTrailingComparisonValue(op, value) &&
2358+
!processor.opts.AllowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing {
2359+
return true
23552360
}
23562361
}
23572362
}
23582363

23592364
return false
23602365
}
23612366

2367+
func (processor *chainProcessor) isSafeTrailingComparisonValue(operator ast.Kind, value *ast.Node) bool {
2368+
if value == nil {
2369+
return false
2370+
}
2371+
2372+
info := processor.getTypeInfo(value)
2373+
2374+
switch operator {
2375+
case ast.KindEqualsEqualsToken:
2376+
return !info.CanBeNullishLike()
2377+
case ast.KindEqualsEqualsEqualsToken:
2378+
return !info.CanBeUndefinedLike()
2379+
case ast.KindExclamationEqualsToken:
2380+
return info.IsAlwaysNullishLike()
2381+
case ast.KindExclamationEqualsEqualsToken:
2382+
return info.IsAlwaysUndefinedLike()
2383+
case ast.KindLessThanToken,
2384+
ast.KindGreaterThanToken,
2385+
ast.KindLessThanEqualsToken,
2386+
ast.KindGreaterThanEqualsToken:
2387+
return false
2388+
default:
2389+
return true
2390+
}
2391+
}
2392+
23622393
func (processor *chainProcessor) validateOrChainNullishChecks(chain []Operand) []Operand {
23632394
if processor.opts.AllowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing {
23642395
return chain

internal/rules/prefer_optional_chain/prefer_optional_chain_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4190,6 +4190,19 @@ foo.bar?.() === undefined || foo.bar?.().baz;
41904190
Code: `request.payload === undefined || request.payload === null`,
41914191
}, rule_tester.ValidTestCase{
41924192
Code: `request.payload === null || request.payload === undefined`,
4193+
}, rule_tester.ValidTestCase{
4194+
Code: `
4195+
type ProjectID = { toString(): string };
4196+
type Intervention = { projectId: ProjectID | null };
4197+
declare const intervention: Intervention;
4198+
declare const previousIntervention: Intervention | null;
4199+
4200+
const isSameProjectAsPreviousIntervention = Boolean(
4201+
intervention.projectId &&
4202+
previousIntervention?.projectId?.toString() ===
4203+
intervention.projectId.toString(),
4204+
);
4205+
`,
41934206
})
41944207

41954208
// --- Spacing sanity checks ---

0 commit comments

Comments
 (0)