Skip to content

Commit f4e1efb

Browse files
authored
Control flow analysis for dependent parameters (#47190)
* Support control flow analysis for dependent parameters * Add tests
1 parent 2f058b7 commit f4e1efb

File tree

5 files changed

+620
-43
lines changed

5 files changed

+620
-43
lines changed

src/compiler/checker.ts

+81-43
Original file line numberDiff line numberDiff line change
@@ -22694,11 +22694,12 @@ namespace ts {
2269422694
return false;
2269522695
}
2269622696

22697-
function getAccessedPropertyName(access: AccessExpression | BindingElement): __String | undefined {
22697+
function getAccessedPropertyName(access: AccessExpression | BindingElement | ParameterDeclaration): __String | undefined {
2269822698
let propertyName;
2269922699
return access.kind === SyntaxKind.PropertyAccessExpression ? access.name.escapedText :
2270022700
access.kind === SyntaxKind.ElementAccessExpression && isStringOrNumericLiteralLike(access.argumentExpression) ? escapeLeadingUnderscores(access.argumentExpression.text) :
2270122701
access.kind === SyntaxKind.BindingElement && (propertyName = getDestructuringPropertyName(access)) ? escapeLeadingUnderscores(propertyName) :
22702+
access.kind === SyntaxKind.Parameter ? ("" + access.parent.parameters.indexOf(access)) as __String :
2270222703
undefined;
2270322704
}
2270422705

@@ -24120,13 +24121,14 @@ namespace ts {
2412024121
}
2412124122

2412224123
function getCandidateDiscriminantPropertyAccess(expr: Expression) {
24123-
if (isBindingPattern(reference)) {
24124-
// When the reference is a binding pattern, we are narrowing a pesudo-reference in getNarrowedTypeOfSymbol.
24125-
// An identifier for a destructuring variable declared in the same binding pattern is a candidate.
24124+
if (isBindingPattern(reference) || isFunctionExpressionOrArrowFunction(reference)) {
24125+
// When the reference is a binding pattern or function or arrow expression, we are narrowing a pesudo-reference in
24126+
// getNarrowedTypeOfSymbol. An identifier for a destructuring variable declared in the same binding pattern or
24127+
// parameter declared in the same parameter list is a candidate.
2412624128
if (isIdentifier(expr)) {
2412724129
const symbol = getResolvedSymbol(expr);
2412824130
const declaration = symbol.valueDeclaration;
24129-
if (declaration && isBindingElement(declaration) && !declaration.initializer && !declaration.dotDotDotToken && reference === declaration.parent) {
24131+
if (declaration && (isBindingElement(declaration) || isParameter(declaration)) && reference === declaration.parent && !declaration.initializer && !declaration.dotDotDotToken) {
2413024132
return declaration;
2413124133
}
2413224134
}
@@ -24173,7 +24175,7 @@ namespace ts {
2417324175
return undefined;
2417424176
}
2417524177

24176-
function narrowTypeByDiscriminant(type: Type, access: AccessExpression | BindingElement, narrowType: (t: Type) => Type): Type {
24178+
function narrowTypeByDiscriminant(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, narrowType: (t: Type) => Type): Type {
2417724179
const propName = getAccessedPropertyName(access);
2417824180
if (propName === undefined) {
2417924181
return type;
@@ -24191,7 +24193,7 @@ namespace ts {
2419124193
});
2419224194
}
2419324195

24194-
function narrowTypeByDiscriminantProperty(type: Type, access: AccessExpression | BindingElement, operator: SyntaxKind, value: Expression, assumeTrue: boolean) {
24196+
function narrowTypeByDiscriminantProperty(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, operator: SyntaxKind, value: Expression, assumeTrue: boolean) {
2419524197
if ((operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) && type.flags & TypeFlags.Union) {
2419624198
const keyPropertyName = getKeyPropertyName(type as UnionType);
2419724199
if (keyPropertyName && keyPropertyName === getAccessedPropertyName(access)) {
@@ -24206,7 +24208,7 @@ namespace ts {
2420624208
return narrowTypeByDiscriminant(type, access, t => narrowTypeByEquality(t, operator, value, assumeTrue));
2420724209
}
2420824210

24209-
function narrowTypeBySwitchOnDiscriminantProperty(type: Type, access: AccessExpression | BindingElement, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) {
24211+
function narrowTypeBySwitchOnDiscriminantProperty(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) {
2421024212
if (clauseStart < clauseEnd && type.flags & TypeFlags.Union && getKeyPropertyName(type as UnionType) === getAccessedPropertyName(access)) {
2421124213
const clauseTypes = getSwitchClauseTypes(switchStatement).slice(clauseStart, clauseEnd);
2421224214
const candidate = getUnionType(map(clauseTypes, t => getConstituentTypeForKeyType(type as UnionType, t) || unknownType));
@@ -24984,42 +24986,78 @@ namespace ts {
2498424986
}
2498524987

2498624988
function getNarrowedTypeOfSymbol(symbol: Symbol, location: Identifier) {
24987-
// If we have a non-rest binding element with no initializer declared as a const variable or a const-like
24988-
// parameter (a parameter for which there are no assignments in the function body), and if the parent type
24989-
// for the destructuring is a union type, one or more of the binding elements may represent discriminant
24990-
// properties, and we want the effects of conditional checks on such discriminants to affect the types of
24991-
// other binding elements from the same destructuring. Consider:
24992-
//
24993-
// type Action =
24994-
// | { kind: 'A', payload: number }
24995-
// | { kind: 'B', payload: string };
24996-
//
24997-
// function f1({ kind, payload }: Action) {
24998-
// if (kind === 'A') {
24999-
// payload.toFixed();
25000-
// }
25001-
// if (kind === 'B') {
25002-
// payload.toUpperCase();
25003-
// }
25004-
// }
25005-
//
25006-
// Above, we want the conditional checks on 'kind' to affect the type of 'payload'. To facilitate this, we use
25007-
// the binding pattern AST instance for '{ kind, payload }' as a pseudo-reference and narrow this reference
25008-
// as if it occurred in the specified location. We then recompute the narrowed binding element type by
25009-
// destructuring from the narrowed parent type.
2501024989
const declaration = symbol.valueDeclaration;
25011-
if (declaration && isBindingElement(declaration) && !declaration.initializer && !declaration.dotDotDotToken && declaration.parent.elements.length >= 2) {
25012-
const parent = declaration.parent.parent;
25013-
if (parent.kind === SyntaxKind.VariableDeclaration && getCombinedNodeFlags(declaration) & NodeFlags.Const || parent.kind === SyntaxKind.Parameter) {
25014-
const links = getNodeLinks(location);
25015-
if (!(links.flags & NodeCheckFlags.InCheckIdentifier)) {
25016-
links.flags |= NodeCheckFlags.InCheckIdentifier;
25017-
const parentType = getTypeForBindingElementParent(parent);
25018-
links.flags &= ~NodeCheckFlags.InCheckIdentifier;
25019-
if (parentType && parentType.flags & TypeFlags.Union && !(parent.kind === SyntaxKind.Parameter && isSymbolAssigned(symbol))) {
25020-
const pattern = declaration.parent;
25021-
const narrowedType = getFlowTypeOfReference(pattern, parentType, parentType, /*flowContainer*/ undefined, location.flowNode);
25022-
return getBindingElementTypeFromParentType(declaration, narrowedType);
24990+
if (declaration) {
24991+
// If we have a non-rest binding element with no initializer declared as a const variable or a const-like
24992+
// parameter (a parameter for which there are no assignments in the function body), and if the parent type
24993+
// for the destructuring is a union type, one or more of the binding elements may represent discriminant
24994+
// properties, and we want the effects of conditional checks on such discriminants to affect the types of
24995+
// other binding elements from the same destructuring. Consider:
24996+
//
24997+
// type Action =
24998+
// | { kind: 'A', payload: number }
24999+
// | { kind: 'B', payload: string };
25000+
//
25001+
// function f({ kind, payload }: Action) {
25002+
// if (kind === 'A') {
25003+
// payload.toFixed();
25004+
// }
25005+
// if (kind === 'B') {
25006+
// payload.toUpperCase();
25007+
// }
25008+
// }
25009+
//
25010+
// Above, we want the conditional checks on 'kind' to affect the type of 'payload'. To facilitate this, we use
25011+
// the binding pattern AST instance for '{ kind, payload }' as a pseudo-reference and narrow this reference
25012+
// as if it occurred in the specified location. We then recompute the narrowed binding element type by
25013+
// destructuring from the narrowed parent type.
25014+
if (isBindingElement(declaration) && !declaration.initializer && !declaration.dotDotDotToken && declaration.parent.elements.length >= 2) {
25015+
const parent = declaration.parent.parent;
25016+
if (parent.kind === SyntaxKind.VariableDeclaration && getCombinedNodeFlags(declaration) & NodeFlags.Const || parent.kind === SyntaxKind.Parameter) {
25017+
const links = getNodeLinks(location);
25018+
if (!(links.flags & NodeCheckFlags.InCheckIdentifier)) {
25019+
links.flags |= NodeCheckFlags.InCheckIdentifier;
25020+
const parentType = getTypeForBindingElementParent(parent);
25021+
links.flags &= ~NodeCheckFlags.InCheckIdentifier;
25022+
if (parentType && parentType.flags & TypeFlags.Union && !(parent.kind === SyntaxKind.Parameter && isSymbolAssigned(symbol))) {
25023+
const pattern = declaration.parent;
25024+
const narrowedType = getFlowTypeOfReference(pattern, parentType, parentType, /*flowContainer*/ undefined, location.flowNode);
25025+
return getBindingElementTypeFromParentType(declaration, narrowedType);
25026+
}
25027+
}
25028+
}
25029+
}
25030+
// If we have a const-like parameter with no type annotation or initializer, and if the parameter is contextually
25031+
// typed by a signature with a single rest parameter of a union of tuple types, one or more of the parameters may
25032+
// represent discriminant tuple elements, and we want the effects of conditional checks on such discriminants to
25033+
// affect the types of other parameters in the same parameter list. Consider:
25034+
//
25035+
// type Action = [kind: 'A', payload: number] | [kind: 'B', payload: string];
25036+
//
25037+
// const f: (...args: Action) => void = (kind, payload) => {
25038+
// if (kind === 'A') {
25039+
// payload.toFixed();
25040+
// }
25041+
// if (kind === 'B') {
25042+
// payload.toUpperCase();
25043+
// }
25044+
// }
25045+
//
25046+
// Above, we want the conditional checks on 'kind' to affect the type of 'payload'. To facilitate this, we use
25047+
// the arrow function AST node for '(kind, payload) => ...' as a pseudo-reference and narrow this reference as
25048+
// if it occurred in the specified location. We then recompute the narrowed parameter type by indexing into the
25049+
// narrowed tuple type.
25050+
if (isParameter(declaration) && !declaration.type && !declaration.initializer && !declaration.dotDotDotToken) {
25051+
const func = declaration.parent;
25052+
if (func.parameters.length >= 2 && isContextSensitiveFunctionOrObjectLiteralMethod(func)) {
25053+
const contextualSignature = getContextualSignature(func);
25054+
if (contextualSignature && contextualSignature.parameters.length === 1 && signatureHasRestParameter(contextualSignature)) {
25055+
const restType = getTypeOfSymbol(contextualSignature.parameters[0]);
25056+
if (restType.flags & TypeFlags.Union && everyType(restType, isTupleType) && !isSymbolAssigned(symbol)) {
25057+
const narrowedType = getFlowTypeOfReference(func, restType, restType, /*flowContainer*/ undefined, location.flowNode);
25058+
const index = func.parameters.indexOf(declaration) - (getThisParameter(func) ? 1 : 0);
25059+
return getIndexedAccessType(narrowedType, getNumberLiteralType(index));
25060+
}
2502325061
}
2502425062
}
2502525063
}

tests/baselines/reference/dependentDestructuredVariables.js

+114
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,64 @@ const { value, done } = it.next();
167167
if (!done) {
168168
value; // number
169169
}
170+
171+
// Repro from #46658
172+
173+
declare function f50(cb: (...args: Args) => void): void
174+
175+
f50((kind, data) => {
176+
if (kind === 'A') {
177+
data.toFixed();
178+
}
179+
if (kind === 'B') {
180+
data.toUpperCase();
181+
}
182+
});
183+
184+
const f51: (...args: ['A', number] | ['B', string]) => void = (kind, payload) => {
185+
if (kind === 'A') {
186+
payload.toFixed();
187+
}
188+
if (kind === 'B') {
189+
payload.toUpperCase();
190+
}
191+
};
192+
193+
const f52: (...args: ['A', number] | ['B']) => void = (kind, payload?) => {
194+
if (kind === 'A') {
195+
payload.toFixed();
196+
}
197+
else {
198+
payload; // undefined
199+
}
200+
};
201+
202+
declare function readFile(path: string, callback: (...args: [err: null, data: unknown[]] | [err: Error, data: undefined]) => void): void;
203+
204+
readFile('hello', (err, data) => {
205+
if (err === null) {
206+
data.length;
207+
}
208+
else {
209+
err.message;
210+
}
211+
});
212+
213+
type ReducerArgs = ["add", { a: number, b: number }] | ["concat", { firstArr: any[], secondArr: any[] }];
214+
215+
const reducer: (...args: ReducerArgs) => void = (op, args) => {
216+
switch (op) {
217+
case "add":
218+
console.log(args.a + args.b);
219+
break;
220+
case "concat":
221+
console.log(args.firstArr.concat(args.secondArr));
222+
break;
223+
}
224+
}
225+
226+
reducer("add", { a: 1, b: 3 });
227+
reducer("concat", { firstArr: [1, 2], secondArr: [3, 4] });
170228

171229

172230
//// [dependentDestructuredVariables.js]
@@ -292,6 +350,50 @@ const { value, done } = it.next();
292350
if (!done) {
293351
value; // number
294352
}
353+
f50((kind, data) => {
354+
if (kind === 'A') {
355+
data.toFixed();
356+
}
357+
if (kind === 'B') {
358+
data.toUpperCase();
359+
}
360+
});
361+
const f51 = (kind, payload) => {
362+
if (kind === 'A') {
363+
payload.toFixed();
364+
}
365+
if (kind === 'B') {
366+
payload.toUpperCase();
367+
}
368+
};
369+
const f52 = (kind, payload) => {
370+
if (kind === 'A') {
371+
payload.toFixed();
372+
}
373+
else {
374+
payload; // undefined
375+
}
376+
};
377+
readFile('hello', (err, data) => {
378+
if (err === null) {
379+
data.length;
380+
}
381+
else {
382+
err.message;
383+
}
384+
});
385+
const reducer = (op, args) => {
386+
switch (op) {
387+
case "add":
388+
console.log(args.a + args.b);
389+
break;
390+
case "concat":
391+
console.log(args.firstArr.concat(args.secondArr));
392+
break;
393+
}
394+
};
395+
reducer("add", { a: 1, b: 3 });
396+
reducer("concat", { firstArr: [1, 2], secondArr: [3, 4] });
295397

296398

297399
//// [dependentDestructuredVariables.d.ts]
@@ -355,3 +457,15 @@ declare type Action3 = {
355457
declare const reducerBroken: (state: number, { type, payload }: Action3) => number;
356458
declare var it: Iterator<number>;
357459
declare const value: any, done: boolean | undefined;
460+
declare function f50(cb: (...args: Args) => void): void;
461+
declare const f51: (...args: ['A', number] | ['B', string]) => void;
462+
declare const f52: (...args: ['A', number] | ['B']) => void;
463+
declare function readFile(path: string, callback: (...args: [err: null, data: unknown[]] | [err: Error, data: undefined]) => void): void;
464+
declare type ReducerArgs = ["add", {
465+
a: number;
466+
b: number;
467+
}] | ["concat", {
468+
firstArr: any[];
469+
secondArr: any[];
470+
}];
471+
declare const reducer: (...args: ReducerArgs) => void;

0 commit comments

Comments
 (0)