Skip to content

Commit ff6626f

Browse files
authored
Merge pull request #34597 from microsoft/optionalChainControlFlow
More optional chaining control flow analysis
2 parents 28028eb + ee846d9 commit ff6626f

File tree

6 files changed

+2040
-65
lines changed

6 files changed

+2040
-65
lines changed

src/compiler/checker.ts

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18770,6 +18770,14 @@ namespace ts {
1877018770
signature.declaration && (getReturnTypeFromAnnotation(signature.declaration) || unknownType).flags & TypeFlags.Never);
1877118771
}
1877218772

18773+
function getTypePredicateArgument(predicate: TypePredicate, callExpression: CallExpression) {
18774+
if (predicate.kind === TypePredicateKind.Identifier || predicate.kind === TypePredicateKind.AssertsIdentifier) {
18775+
return callExpression.arguments[predicate.parameterIndex];
18776+
}
18777+
const invokedExpression = skipParentheses(callExpression.expression);
18778+
return isAccessExpression(invokedExpression) ? skipParentheses(invokedExpression.expression) : undefined;
18779+
}
18780+
1877318781
function reportFlowControlError(node: Node) {
1877418782
const block = <Block | ModuleBlock | SourceFile>findAncestor(node, isFunctionOrModuleBlock);
1877518783
const sourceFile = getSourceFileOfNode(node);
@@ -19158,20 +19166,30 @@ namespace ts {
1915819166
if (isMatchingReference(reference, expr)) {
1915919167
type = narrowTypeBySwitchOnDiscriminant(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd);
1916019168
}
19161-
else if (isMatchingReferenceDiscriminant(expr, type)) {
19162-
type = narrowTypeByDiscriminant(
19163-
type,
19164-
expr as AccessExpression,
19165-
t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd));
19166-
}
1916719169
else if (expr.kind === SyntaxKind.TypeOfExpression && isMatchingReference(reference, (expr as TypeOfExpression).expression)) {
1916819170
type = narrowBySwitchOnTypeOf(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd);
1916919171
}
19170-
else if (containsMatchingReferenceDiscriminant(reference, expr)) {
19171-
type = declaredType;
19172-
}
19173-
else if (flow.clauseStart === flow.clauseEnd && isExhaustiveSwitchStatement(flow.switchStatement)) {
19174-
return unreachableNeverType;
19172+
else {
19173+
if (strictNullChecks) {
19174+
if (optionalChainContainsReference(expr, reference)) {
19175+
type = narrowTypeBySwitchOptionalChainContainment(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd,
19176+
t => !(t.flags & (TypeFlags.Undefined | TypeFlags.Never)));
19177+
}
19178+
else if (expr.kind === SyntaxKind.TypeOfExpression && optionalChainContainsReference((expr as TypeOfExpression).expression, reference)) {
19179+
type = narrowTypeBySwitchOptionalChainContainment(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd,
19180+
t => !(t.flags & TypeFlags.Never || t.flags & TypeFlags.StringLiteral && (<StringLiteralType>t).value === "undefined"));
19181+
}
19182+
}
19183+
if (isMatchingReferenceDiscriminant(expr, type)) {
19184+
type = narrowTypeByDiscriminant(type, expr as AccessExpression,
19185+
t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd));
19186+
}
19187+
else if (containsMatchingReferenceDiscriminant(reference, expr)) {
19188+
type = declaredType;
19189+
}
19190+
else if (flow.clauseStart === flow.clauseEnd && isExhaustiveSwitchStatement(flow.switchStatement)) {
19191+
return unreachableNeverType;
19192+
}
1917519193
}
1917619194
return createFlowType(type, isIncomplete(flowType));
1917719195
}
@@ -19316,6 +19334,9 @@ namespace ts {
1931619334
if (isMatchingReference(reference, expr)) {
1931719335
return getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy);
1931819336
}
19337+
if (strictNullChecks && assumeTrue && optionalChainContainsReference(expr, reference)) {
19338+
type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
19339+
}
1931919340
if (isMatchingReferenceDiscriminant(expr, declaredType)) {
1932019341
return narrowTypeByDiscriminant(type, <AccessExpression>expr, t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy));
1932119342
}
@@ -19367,12 +19388,12 @@ namespace ts {
1936719388
if (isMatchingReference(reference, right)) {
1936819389
return narrowTypeByEquality(type, operator, left, assumeTrue);
1936919390
}
19370-
if (assumeTrue && strictNullChecks) {
19391+
if (strictNullChecks) {
1937119392
if (optionalChainContainsReference(left, reference)) {
19372-
type = narrowTypeByOptionalChainContainment(type, operator, right);
19393+
type = narrowTypeByOptionalChainContainment(type, operator, right, assumeTrue);
1937319394
}
1937419395
else if (optionalChainContainsReference(right, reference)) {
19375-
type = narrowTypeByOptionalChainContainment(type, operator, left);
19396+
type = narrowTypeByOptionalChainContainment(type, operator, left, assumeTrue);
1937619397
}
1937719398
}
1937819399
if (isMatchingReferenceDiscriminant(left, declaredType)) {
@@ -19399,16 +19420,14 @@ namespace ts {
1939919420
return type;
1940019421
}
1940119422

19402-
function narrowTypeByOptionalChainContainment(type: Type, operator: SyntaxKind, value: Expression): Type {
19403-
// We are in the true branch of obj?.foo === value or obj?.foo !== value. We remove undefined and null from
19423+
function narrowTypeByOptionalChainContainment(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type {
19424+
// We are in a branch of obj?.foo === value or obj?.foo !== value. We remove undefined and null from
1940419425
// the type of obj if (a) the operator is === and the type of value doesn't include undefined or (b) the
1940519426
// operator is !== and the type of value is undefined.
19406-
const valueType = getTypeOfExpression(value);
19407-
return operator === SyntaxKind.EqualsEqualsToken && !(getTypeFacts(valueType) & TypeFacts.EQUndefinedOrNull) ||
19408-
operator === SyntaxKind.EqualsEqualsEqualsToken && !(getTypeFacts(valueType) & TypeFacts.EQUndefined) ||
19409-
operator === SyntaxKind.ExclamationEqualsToken && valueType.flags & TypeFlags.Nullable ||
19410-
operator === SyntaxKind.ExclamationEqualsEqualsToken && valueType.flags & TypeFlags.Undefined ?
19411-
getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type;
19427+
const effectiveTrue = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.EqualsEqualsEqualsToken ? assumeTrue : !assumeTrue;
19428+
const doubleEquals = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken;
19429+
const valueNonNullish = !(getTypeFacts(getTypeOfExpression(value)) & (doubleEquals ? TypeFacts.EQUndefinedOrNull : TypeFacts.EQUndefined));
19430+
return effectiveTrue === valueNonNullish ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type;
1941219431
}
1941319432

1941419433
function narrowTypeByEquality(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type {
@@ -19459,10 +19478,12 @@ namespace ts {
1945919478

1946019479
function narrowTypeByTypeof(type: Type, typeOfExpr: TypeOfExpression, operator: SyntaxKind, literal: LiteralExpression, assumeTrue: boolean): Type {
1946119480
// We have '==', '!=', '===', or !==' operator with 'typeof xxx' and string literal operands
19481+
if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) {
19482+
assumeTrue = !assumeTrue;
19483+
}
1946219484
const target = getReferenceCandidate(typeOfExpr.expression);
1946319485
if (!isMatchingReference(reference, target)) {
19464-
if (assumeTrue && (operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.EqualsEqualsEqualsToken) &&
19465-
strictNullChecks && optionalChainContainsReference(target, reference)) {
19486+
if (strictNullChecks && optionalChainContainsReference(target, reference) && assumeTrue === (literal.text !== "undefined")) {
1946619487
return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
1946719488
}
1946819489
// For a reference of the form 'x.y', a 'typeof x === ...' type guard resets the
@@ -19472,9 +19493,6 @@ namespace ts {
1947219493
}
1947319494
return type;
1947419495
}
19475-
if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) {
19476-
assumeTrue = !assumeTrue;
19477-
}
1947819496
if (type.flags & TypeFlags.Any && literal.text === "function") {
1947919497
return type;
1948019498
}
@@ -19509,6 +19527,11 @@ namespace ts {
1950919527
}
1951019528
}
1951119529

19530+
function narrowTypeBySwitchOptionalChainContainment(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number, clauseCheck: (type: Type) => boolean) {
19531+
const everyClauseChecks = clauseStart !== clauseEnd && every(getSwitchClauseTypes(switchStatement).slice(clauseStart, clauseEnd), clauseCheck);
19532+
return everyClauseChecks ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type;
19533+
}
19534+
1951219535
function narrowTypeBySwitchOnDiscriminant(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) {
1951319536
// We only narrow if all case expressions specify
1951419537
// values with unit types, except for the case where
@@ -19729,28 +19752,17 @@ namespace ts {
1972919752

1973019753
function narrowTypeByTypePredicate(type: Type, predicate: TypePredicate, callExpression: CallExpression, assumeTrue: boolean): Type {
1973119754
// Don't narrow from 'any' if the predicate type is exactly 'Object' or 'Function'
19732-
if (isTypeAny(type) && (predicate.type === globalObjectType || predicate.type === globalFunctionType)) {
19733-
return type;
19734-
}
19735-
if (predicate.kind === TypePredicateKind.Identifier || predicate.kind === TypePredicateKind.AssertsIdentifier) {
19736-
const predicateArgument = callExpression.arguments[predicate.parameterIndex];
19737-
if (predicateArgument && predicate.type) {
19755+
if (predicate.type && !(isTypeAny(type) && (predicate.type === globalObjectType || predicate.type === globalFunctionType))) {
19756+
const predicateArgument = getTypePredicateArgument(predicate, callExpression);
19757+
if (predicateArgument) {
1973819758
if (isMatchingReference(reference, predicateArgument)) {
1973919759
return getNarrowedType(type, predicate.type, assumeTrue, isTypeSubtypeOf);
1974019760
}
19741-
if (containsMatchingReference(reference, predicateArgument)) {
19742-
return declaredType;
19743-
}
19744-
}
19745-
}
19746-
else {
19747-
const invokedExpression = skipParentheses(callExpression.expression);
19748-
if (isAccessExpression(invokedExpression) && predicate.type) {
19749-
const possibleReference = skipParentheses(invokedExpression.expression);
19750-
if (isMatchingReference(reference, possibleReference)) {
19751-
return getNarrowedType(type, predicate.type, assumeTrue, isTypeSubtypeOf);
19761+
if (strictNullChecks && assumeTrue && optionalChainContainsReference(predicateArgument, reference) &&
19762+
!(getTypeFacts(predicate.type) & TypeFacts.EQUndefined)) {
19763+
return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
1975219764
}
19753-
if (containsMatchingReference(reference, possibleReference)) {
19765+
if (containsMatchingReference(reference, predicateArgument)) {
1975419766
return declaredType;
1975519767
}
1975619768
}

0 commit comments

Comments
 (0)