Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control flow analysis for dependent parameters #47190

Merged
merged 2 commits into from
Jan 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 81 additions & 43 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22679,11 +22679,12 @@ namespace ts {
return false;
}

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

Expand Down Expand Up @@ -24105,13 +24106,14 @@ namespace ts {
}

function getCandidateDiscriminantPropertyAccess(expr: Expression) {
if (isBindingPattern(reference)) {
// When the reference is a binding pattern, we are narrowing a pesudo-reference in getNarrowedTypeOfSymbol.
// An identifier for a destructuring variable declared in the same binding pattern is a candidate.
if (isBindingPattern(reference) || isFunctionExpressionOrArrowFunction(reference)) {
// When the reference is a binding pattern or function or arrow expression, we are narrowing a pesudo-reference in
// getNarrowedTypeOfSymbol. An identifier for a destructuring variable declared in the same binding pattern or
// parameter declared in the same parameter list is a candidate.
if (isIdentifier(expr)) {
const symbol = getResolvedSymbol(expr);
const declaration = symbol.valueDeclaration;
if (declaration && isBindingElement(declaration) && !declaration.initializer && !declaration.dotDotDotToken && reference === declaration.parent) {
if (declaration && (isBindingElement(declaration) || isParameter(declaration)) && reference === declaration.parent && !declaration.initializer && !declaration.dotDotDotToken) {
return declaration;
}
}
Expand Down Expand Up @@ -24158,7 +24160,7 @@ namespace ts {
return undefined;
}

function narrowTypeByDiscriminant(type: Type, access: AccessExpression | BindingElement, narrowType: (t: Type) => Type): Type {
function narrowTypeByDiscriminant(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, narrowType: (t: Type) => Type): Type {
const propName = getAccessedPropertyName(access);
if (propName === undefined) {
return type;
Expand All @@ -24176,7 +24178,7 @@ namespace ts {
});
}

function narrowTypeByDiscriminantProperty(type: Type, access: AccessExpression | BindingElement, operator: SyntaxKind, value: Expression, assumeTrue: boolean) {
function narrowTypeByDiscriminantProperty(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, operator: SyntaxKind, value: Expression, assumeTrue: boolean) {
if ((operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) && type.flags & TypeFlags.Union) {
const keyPropertyName = getKeyPropertyName(type as UnionType);
if (keyPropertyName && keyPropertyName === getAccessedPropertyName(access)) {
Expand All @@ -24191,7 +24193,7 @@ namespace ts {
return narrowTypeByDiscriminant(type, access, t => narrowTypeByEquality(t, operator, value, assumeTrue));
}

function narrowTypeBySwitchOnDiscriminantProperty(type: Type, access: AccessExpression | BindingElement, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) {
function narrowTypeBySwitchOnDiscriminantProperty(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) {
if (clauseStart < clauseEnd && type.flags & TypeFlags.Union && getKeyPropertyName(type as UnionType) === getAccessedPropertyName(access)) {
const clauseTypes = getSwitchClauseTypes(switchStatement).slice(clauseStart, clauseEnd);
const candidate = getUnionType(map(clauseTypes, t => getConstituentTypeForKeyType(type as UnionType, t) || unknownType));
Expand Down Expand Up @@ -24969,42 +24971,78 @@ namespace ts {
}

function getNarrowedTypeOfSymbol(symbol: Symbol, location: Identifier) {
// If we have a non-rest binding element with no initializer declared as a const variable or a const-like
// parameter (a parameter for which there are no assignments in the function body), and if the parent type
// for the destructuring is a union type, one or more of the binding elements may represent discriminant
// properties, and we want the effects of conditional checks on such discriminants to affect the types of
// other binding elements from the same destructuring. Consider:
//
// type Action =
// | { kind: 'A', payload: number }
// | { kind: 'B', payload: string };
//
// function f1({ kind, payload }: Action) {
// if (kind === 'A') {
// payload.toFixed();
// }
// if (kind === 'B') {
// payload.toUpperCase();
// }
// }
//
// Above, we want the conditional checks on 'kind' to affect the type of 'payload'. To facilitate this, we use
// the binding pattern AST instance for '{ kind, payload }' as a pseudo-reference and narrow this reference
// as if it occurred in the specified location. We then recompute the narrowed binding element type by
// destructuring from the narrowed parent type.
const declaration = symbol.valueDeclaration;
if (declaration && isBindingElement(declaration) && !declaration.initializer && !declaration.dotDotDotToken && declaration.parent.elements.length >= 2) {
const parent = declaration.parent.parent;
if (parent.kind === SyntaxKind.VariableDeclaration && getCombinedNodeFlags(declaration) & NodeFlags.Const || parent.kind === SyntaxKind.Parameter) {
const links = getNodeLinks(location);
if (!(links.flags & NodeCheckFlags.InCheckIdentifier)) {
links.flags |= NodeCheckFlags.InCheckIdentifier;
const parentType = getTypeForBindingElementParent(parent);
links.flags &= ~NodeCheckFlags.InCheckIdentifier;
if (parentType && parentType.flags & TypeFlags.Union && !(parent.kind === SyntaxKind.Parameter && isSymbolAssigned(symbol))) {
const pattern = declaration.parent;
const narrowedType = getFlowTypeOfReference(pattern, parentType, parentType, /*flowContainer*/ undefined, location.flowNode);
return getBindingElementTypeFromParentType(declaration, narrowedType);
if (declaration) {
// If we have a non-rest binding element with no initializer declared as a const variable or a const-like
// parameter (a parameter for which there are no assignments in the function body), and if the parent type
// for the destructuring is a union type, one or more of the binding elements may represent discriminant
// properties, and we want the effects of conditional checks on such discriminants to affect the types of
// other binding elements from the same destructuring. Consider:
//
// type Action =
// | { kind: 'A', payload: number }
// | { kind: 'B', payload: string };
//
// function f({ kind, payload }: Action) {
// if (kind === 'A') {
// payload.toFixed();
// }
// if (kind === 'B') {
// payload.toUpperCase();
// }
// }
//
// Above, we want the conditional checks on 'kind' to affect the type of 'payload'. To facilitate this, we use
// the binding pattern AST instance for '{ kind, payload }' as a pseudo-reference and narrow this reference
// as if it occurred in the specified location. We then recompute the narrowed binding element type by
// destructuring from the narrowed parent type.
if (isBindingElement(declaration) && !declaration.initializer && !declaration.dotDotDotToken && declaration.parent.elements.length >= 2) {
const parent = declaration.parent.parent;
if (parent.kind === SyntaxKind.VariableDeclaration && getCombinedNodeFlags(declaration) & NodeFlags.Const || parent.kind === SyntaxKind.Parameter) {
const links = getNodeLinks(location);
if (!(links.flags & NodeCheckFlags.InCheckIdentifier)) {
links.flags |= NodeCheckFlags.InCheckIdentifier;
const parentType = getTypeForBindingElementParent(parent);
links.flags &= ~NodeCheckFlags.InCheckIdentifier;
if (parentType && parentType.flags & TypeFlags.Union && !(parent.kind === SyntaxKind.Parameter && isSymbolAssigned(symbol))) {
const pattern = declaration.parent;
const narrowedType = getFlowTypeOfReference(pattern, parentType, parentType, /*flowContainer*/ undefined, location.flowNode);
return getBindingElementTypeFromParentType(declaration, narrowedType);
}
}
}
}
// If we have a const-like parameter with no type annotation or initializer, and if the parameter is contextually
// typed by a signature with a single rest parameter of a union of tuple types, one or more of the parameters may
// represent discriminant tuple elements, and we want the effects of conditional checks on such discriminants to
// affect the types of other parameters in the same parameter list. Consider:
//
// type Action = [kind: 'A', payload: number] | [kind: 'B', payload: string];
//
// const f: (...args: Action) => void = (kind, payload) => {
// if (kind === 'A') {
// payload.toFixed();
// }
// if (kind === 'B') {
// payload.toUpperCase();
// }
// }
//
// Above, we want the conditional checks on 'kind' to affect the type of 'payload'. To facilitate this, we use
// the arrow function AST node for '(kind, payload) => ...' as a pseudo-reference and narrow this reference as
// if it occurred in the specified location. We then recompute the narrowed parameter type by indexing into the
// narrowed tuple type.
if (isParameter(declaration) && !declaration.type && !declaration.initializer && !declaration.dotDotDotToken) {
const func = declaration.parent;
if (func.parameters.length >= 2 && isContextSensitiveFunctionOrObjectLiteralMethod(func)) {
const contextualSignature = getContextualSignature(func);
if (contextualSignature && contextualSignature.parameters.length === 1 && signatureHasRestParameter(contextualSignature)) {
const restType = getTypeOfSymbol(contextualSignature.parameters[0]);
if (restType.flags & TypeFlags.Union && everyType(restType, isTupleType) && !isSymbolAssigned(symbol)) {
const narrowedType = getFlowTypeOfReference(func, restType, restType, /*flowContainer*/ undefined, location.flowNode);
const index = func.parameters.indexOf(declaration) - (getThisParameter(func) ? 1 : 0);
return getIndexedAccessType(narrowedType, getNumberLiteralType(index));
}
}
}
}
Expand Down
114 changes: 114 additions & 0 deletions tests/baselines/reference/dependentDestructuredVariables.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,64 @@ const { value, done } = it.next();
if (!done) {
value; // number
}

// Repro from #46658

declare function f50(cb: (...args: Args) => void): void

f50((kind, data) => {
if (kind === 'A') {
data.toFixed();
}
if (kind === 'B') {
data.toUpperCase();
}
});

const f51: (...args: ['A', number] | ['B', string]) => void = (kind, payload) => {
if (kind === 'A') {
payload.toFixed();
}
if (kind === 'B') {
payload.toUpperCase();
}
};

const f52: (...args: ['A', number] | ['B']) => void = (kind, payload?) => {
if (kind === 'A') {
payload.toFixed();
}
else {
payload; // undefined
}
};

declare function readFile(path: string, callback: (...args: [err: null, data: unknown[]] | [err: Error, data: undefined]) => void): void;

readFile('hello', (err, data) => {
if (err === null) {
data.length;
}
else {
err.message;
}
});

type ReducerArgs = ["add", { a: number, b: number }] | ["concat", { firstArr: any[], secondArr: any[] }];

const reducer: (...args: ReducerArgs) => void = (op, args) => {
switch (op) {
case "add":
console.log(args.a + args.b);
break;
case "concat":
console.log(args.firstArr.concat(args.secondArr));
break;
}
}

reducer("add", { a: 1, b: 3 });
reducer("concat", { firstArr: [1, 2], secondArr: [3, 4] });


//// [dependentDestructuredVariables.js]
Expand Down Expand Up @@ -292,6 +350,50 @@ const { value, done } = it.next();
if (!done) {
value; // number
}
f50((kind, data) => {
if (kind === 'A') {
data.toFixed();
}
if (kind === 'B') {
data.toUpperCase();
}
});
const f51 = (kind, payload) => {
if (kind === 'A') {
payload.toFixed();
}
if (kind === 'B') {
payload.toUpperCase();
}
};
const f52 = (kind, payload) => {
if (kind === 'A') {
payload.toFixed();
}
else {
payload; // undefined
}
};
readFile('hello', (err, data) => {
if (err === null) {
data.length;
}
else {
err.message;
}
});
const reducer = (op, args) => {
switch (op) {
case "add":
console.log(args.a + args.b);
break;
case "concat":
console.log(args.firstArr.concat(args.secondArr));
break;
}
};
reducer("add", { a: 1, b: 3 });
reducer("concat", { firstArr: [1, 2], secondArr: [3, 4] });


//// [dependentDestructuredVariables.d.ts]
Expand Down Expand Up @@ -355,3 +457,15 @@ declare type Action3 = {
declare const reducerBroken: (state: number, { type, payload }: Action3) => number;
declare var it: Iterator<number>;
declare const value: any, done: boolean | undefined;
declare function f50(cb: (...args: Args) => void): void;
declare const f51: (...args: ['A', number] | ['B', string]) => void;
declare const f52: (...args: ['A', number] | ['B']) => void;
declare function readFile(path: string, callback: (...args: [err: null, data: unknown[]] | [err: Error, data: undefined]) => void): void;
declare type ReducerArgs = ["add", {
a: number;
b: number;
}] | ["concat", {
firstArr: any[];
secondArr: any[];
}];
declare const reducer: (...args: ReducerArgs) => void;
Loading