Skip to content

Commit 831b770

Browse files
authored
Control flow analysis for destructured discriminated unions (microsoft#46266)
* CFA for dependent variables destructured from discriminated union * Accept new baselines * Add tests * Limit calls to isSymbolAssigned * Fix wrong operator
1 parent de23842 commit 831b770

File tree

8 files changed

+1457
-50
lines changed

8 files changed

+1457
-50
lines changed

src/compiler/checker.ts

Lines changed: 106 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8544,12 +8544,16 @@ namespace ts {
85448544

85458545
/** Return the inferred type for a binding element */
85468546
function getTypeForBindingElement(declaration: BindingElement): Type | undefined {
8547-
const pattern = declaration.parent;
8548-
let parentType = getTypeForBindingElementParent(pattern.parent);
8549-
// If no type or an any type was inferred for parent, infer that for the binding element
8550-
if (!parentType || isTypeAny(parentType)) {
8547+
const parentType = getTypeForBindingElementParent(declaration.parent.parent);
8548+
return parentType && getBindingElementTypeFromParentType(declaration, parentType);
8549+
}
8550+
8551+
function getBindingElementTypeFromParentType(declaration: BindingElement, parentType: Type): Type {
8552+
// If an any type was inferred for parent, infer that for the binding element
8553+
if (isTypeAny(parentType)) {
85518554
return parentType;
85528555
}
8556+
const pattern = declaration.parent;
85538557
// Relax null check on ambient destructuring parameters, since the parameters have no implementation and are just documentation
85548558
if (strictNullChecks && declaration.flags & NodeFlags.Ambient && isParameterDeclaration(declaration)) {
85558559
parentType = getNonNullableType(parentType);
@@ -22633,30 +22637,6 @@ namespace ts {
2263322637
return false;
2263422638
}
2263522639

22636-
function getPropertyAccess(expr: Expression) {
22637-
if (isAccessExpression(expr)) {
22638-
return expr;
22639-
}
22640-
if (isIdentifier(expr)) {
22641-
const symbol = getResolvedSymbol(expr);
22642-
if (isConstVariable(symbol)) {
22643-
const declaration = symbol.valueDeclaration!;
22644-
// Given 'const x = obj.kind', allow 'x' as an alias for 'obj.kind'
22645-
if (isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && isAccessExpression(declaration.initializer)) {
22646-
return declaration.initializer;
22647-
}
22648-
// Given 'const { kind: x } = obj', allow 'x' as an alias for 'obj.kind'
22649-
if (isBindingElement(declaration) && !declaration.initializer) {
22650-
const parent = declaration.parent.parent;
22651-
if (isVariableDeclaration(parent) && !parent.type && parent.initializer && (isIdentifier(parent.initializer) || isAccessExpression(parent.initializer))) {
22652-
return declaration;
22653-
}
22654-
}
22655-
}
22656-
}
22657-
return undefined;
22658-
}
22659-
2266022640
function getAccessedPropertyName(access: AccessExpression | BindingElement): __String | undefined {
2266122641
let propertyName;
2266222642
return access.kind === SyntaxKind.PropertyAccessExpression ? access.name.escapedText :
@@ -23626,19 +23606,19 @@ namespace ts {
2362623606
return false;
2362723607
}
2362823608

23629-
function getFlowTypeOfReference(reference: Node, declaredType: Type, initialType = declaredType, flowContainer?: Node) {
23609+
function getFlowTypeOfReference(reference: Node, declaredType: Type, initialType = declaredType, flowContainer?: Node, flowNode = reference.flowNode) {
2363023610
let key: string | undefined;
2363123611
let isKeySet = false;
2363223612
let flowDepth = 0;
2363323613
if (flowAnalysisDisabled) {
2363423614
return errorType;
2363523615
}
23636-
if (!reference.flowNode) {
23616+
if (!flowNode) {
2363723617
return declaredType;
2363823618
}
2363923619
flowInvocationCount++;
2364023620
const sharedFlowStart = sharedFlowCount;
23641-
const evolvedType = getTypeFromFlowType(getTypeAtFlowNode(reference.flowNode));
23621+
const evolvedType = getTypeFromFlowType(getTypeAtFlowNode(flowNode));
2364223622
sharedFlowCount = sharedFlowStart;
2364323623
// When the reference is 'x' in an 'x.length', 'x.push(value)', 'x.unshift(value)' or x[n] = value' operation,
2364423624
// we give type 'any[]' to 'x' instead of using the type determined by control flow analysis such that operations
@@ -24082,13 +24062,58 @@ namespace ts {
2408224062
return result;
2408324063
}
2408424064

24065+
function getCandidateDiscriminantPropertyAccess(expr: Expression) {
24066+
if (isBindingPattern(reference)) {
24067+
// When the reference is a binding pattern, we are narrowing a pesudo-reference in getNarrowedTypeOfSymbol.
24068+
// An identifier for a destructuring variable declared in the same binding pattern is a candidate.
24069+
if (isIdentifier(expr)) {
24070+
const symbol = getResolvedSymbol(expr);
24071+
const declaration = symbol.valueDeclaration;
24072+
if (declaration && isBindingElement(declaration) && !declaration.initializer && !declaration.dotDotDotToken && reference === declaration.parent) {
24073+
return declaration;
24074+
}
24075+
}
24076+
}
24077+
else if (isAccessExpression(expr)) {
24078+
// An access expression is a candidate if the reference matches the left hand expression.
24079+
if (isMatchingReference(reference, expr.expression)) {
24080+
return expr;
24081+
}
24082+
}
24083+
else if (isIdentifier(expr)) {
24084+
const symbol = getResolvedSymbol(expr);
24085+
if (isConstVariable(symbol)) {
24086+
const declaration = symbol.valueDeclaration!;
24087+
// Given 'const x = obj.kind', allow 'x' as an alias for 'obj.kind'
24088+
if (isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && isAccessExpression(declaration.initializer) &&
24089+
isMatchingReference(reference, declaration.initializer.expression)) {
24090+
return declaration.initializer;
24091+
}
24092+
// Given 'const { kind: x } = obj', allow 'x' as an alias for 'obj.kind'
24093+
if (isBindingElement(declaration) && !declaration.initializer) {
24094+
const parent = declaration.parent.parent;
24095+
if (isVariableDeclaration(parent) && !parent.type && parent.initializer && (isIdentifier(parent.initializer) || isAccessExpression(parent.initializer)) &&
24096+
isMatchingReference(reference, parent.initializer)) {
24097+
return declaration;
24098+
}
24099+
}
24100+
}
24101+
}
24102+
return undefined;
24103+
}
24104+
2408524105
function getDiscriminantPropertyAccess(expr: Expression, computedType: Type) {
24086-
let access, name;
2408724106
const type = declaredType.flags & TypeFlags.Union ? declaredType : computedType;
24088-
return type.flags & TypeFlags.Union && (access = getPropertyAccess(expr)) && (name = getAccessedPropertyName(access)) &&
24089-
isMatchingReference(reference, isAccessExpression(access) ? access.expression : access.parent.parent.initializer!) &&
24090-
isDiscriminantProperty(type, name) ?
24091-
access : undefined;
24107+
if (type.flags & TypeFlags.Union) {
24108+
const access = getCandidateDiscriminantPropertyAccess(expr);
24109+
if (access) {
24110+
const name = getAccessedPropertyName(access);
24111+
if (name && isDiscriminantProperty(type, name)) {
24112+
return access;
24113+
}
24114+
}
24115+
}
24116+
return undefined;
2409224117
}
2409324118

2409424119
function narrowTypeByDiscriminant(type: Type, access: AccessExpression | BindingElement, narrowType: (t: Type) => Type): Type {
@@ -24901,6 +24926,50 @@ namespace ts {
2490124926
}
2490224927
}
2490324928

24929+
function getNarrowedTypeOfSymbol(symbol: Symbol, location: Identifier) {
24930+
// If we have a non-rest binding element with no initializer declared as a const variable or a const-like
24931+
// parameter (a parameter for which there are no assignments in the function body), and if the parent type
24932+
// for the destructuring is a union type, one or more of the binding elements may represent discriminant
24933+
// properties, and we want the effects of conditional checks on such discriminants to affect the types of
24934+
// other binding elements from the same destructuring. Consider:
24935+
//
24936+
// type Action =
24937+
// | { kind: 'A', payload: number }
24938+
// | { kind: 'B', payload: string };
24939+
//
24940+
// function f1({ kind, payload }: Action) {
24941+
// if (kind === 'A') {
24942+
// payload.toFixed();
24943+
// }
24944+
// if (kind === 'B') {
24945+
// payload.toUpperCase();
24946+
// }
24947+
// }
24948+
//
24949+
// Above, we want the conditional checks on 'kind' to affect the type of 'payload'. To facilitate this, we use
24950+
// the binding pattern AST instance for '{ kind, payload }' as a pseudo-reference and narrow this reference
24951+
// as if it occurred in the specified location. We then recompute the narrowed binding element type by
24952+
// destructuring from the narrowed parent type.
24953+
const declaration = symbol.valueDeclaration;
24954+
if (declaration && isBindingElement(declaration) && !declaration.initializer && !declaration.dotDotDotToken && declaration.parent.elements.length >= 2) {
24955+
const parent = declaration.parent.parent;
24956+
if (parent.kind === SyntaxKind.VariableDeclaration && getCombinedNodeFlags(declaration) & NodeFlags.Const || parent.kind === SyntaxKind.Parameter) {
24957+
const links = getNodeLinks(location);
24958+
if (!(links.flags & NodeCheckFlags.InCheckIdentifier)) {
24959+
links.flags |= NodeCheckFlags.InCheckIdentifier;
24960+
const parentType = getTypeForBindingElementParent(parent);
24961+
links.flags &= ~NodeCheckFlags.InCheckIdentifier;
24962+
if (parentType && parentType.flags & TypeFlags.Union && !(parent.kind === SyntaxKind.Parameter && isSymbolAssigned(symbol))) {
24963+
const pattern = declaration.parent;
24964+
const narrowedType = getFlowTypeOfReference(pattern, parentType, parentType, /*flowContainer*/ undefined, location.flowNode);
24965+
return getBindingElementTypeFromParentType(declaration, narrowedType);
24966+
}
24967+
}
24968+
}
24969+
}
24970+
return getTypeOfSymbol(symbol);
24971+
}
24972+
2490424973
function checkIdentifier(node: Identifier, checkMode: CheckMode | undefined): Type {
2490524974
const symbol = getResolvedSymbol(node);
2490624975
if (symbol === unknownSymbol) {
@@ -24984,7 +25053,7 @@ namespace ts {
2498425053

2498525054
checkNestedBlockScopedBinding(node, symbol);
2498625055

24987-
let type = getTypeOfSymbol(localOrExportSymbol);
25056+
let type = getNarrowedTypeOfSymbol(localOrExportSymbol, node);
2498825057
const assignmentKind = getAssignmentTargetKind(node);
2498925058

2499025059
if (assignmentKind) {

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5083,6 +5083,7 @@ namespace ts {
50835083
ConstructorReferenceInClass = 0x02000000, // Binding to a class constructor inside of the class's body.
50845084
ContainsClassWithPrivateIdentifiers = 0x04000000, // Marked on all block-scoped containers containing a class with private identifiers.
50855085
ContainsSuperPropertyInStaticInitializer = 0x08000000, // Marked on all block-scoped containers containing a static initializer with 'super.x' or 'super[x]'.
5086+
InCheckIdentifier = 0x10000000,
50865087
}
50875088

50885089
/* @internal */

tests/baselines/reference/controlFlowAliasing.errors.txt

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,11 @@ tests/cases/conformance/controlFlow/controlFlowAliasing.ts(232,13): error TS2322
2828
Type 'number' is not assignable to type 'string'.
2929
tests/cases/conformance/controlFlow/controlFlowAliasing.ts(233,13): error TS2322: Type 'string | number' is not assignable to type 'string'.
3030
Type 'number' is not assignable to type 'string'.
31-
tests/cases/conformance/controlFlow/controlFlowAliasing.ts(267,13): error TS2322: Type 'string | number' is not assignable to type 'string'.
32-
Type 'number' is not assignable to type 'string'.
33-
tests/cases/conformance/controlFlow/controlFlowAliasing.ts(270,13): error TS2322: Type 'string | number' is not assignable to type 'number'.
34-
Type 'string' is not assignable to type 'number'.
3531
tests/cases/conformance/controlFlow/controlFlowAliasing.ts(280,5): error TS2448: Block-scoped variable 'a' used before its declaration.
3632
tests/cases/conformance/controlFlow/controlFlowAliasing.ts(280,5): error TS2454: Variable 'a' is used before being assigned.
3733

3834

39-
==== tests/cases/conformance/controlFlow/controlFlowAliasing.ts (19 errors) ====
35+
==== tests/cases/conformance/controlFlow/controlFlowAliasing.ts (17 errors) ====
4036
// Narrowing by aliased conditional expressions
4137

4238
function f10(x: string | number) {
@@ -349,15 +345,9 @@ tests/cases/conformance/controlFlow/controlFlowAliasing.ts(280,5): error TS2454:
349345
function foo({ kind, payload }: Data) {
350346
if (kind === 'str') {
351347
let t: string = payload;
352-
~
353-
!!! error TS2322: Type 'string | number' is not assignable to type 'string'.
354-
!!! error TS2322: Type 'number' is not assignable to type 'string'.
355348
}
356349
else {
357350
let t: number = payload;
358-
~
359-
!!! error TS2322: Type 'string | number' is not assignable to type 'number'.
360-
!!! error TS2322: Type 'string' is not assignable to type 'number'.
361351
}
362352
}
363353

tests/baselines/reference/controlFlowAliasing.types

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -888,12 +888,12 @@ function foo({ kind, payload }: Data) {
888888

889889
let t: string = payload;
890890
>t : string
891-
>payload : string | number
891+
>payload : string
892892
}
893893
else {
894894
let t: number = payload;
895895
>t : number
896-
>payload : string | number
896+
>payload : number
897897
}
898898
}
899899

0 commit comments

Comments
 (0)