Preserve type refinements in closures created past last assignment (#56908)

This commit is contained in:
Anders Hejlsberg
2024-01-08 20:21:05 -08:00
committed by GitHub
parent f9cb96c03d
commit 8ff77fbc48
20 changed files with 1485 additions and 68 deletions
+102 -20
View File
@@ -668,7 +668,6 @@ import {
isOutermostOptionalChain,
isParameter,
isParameterDeclaration,
isParameterOrCatchClauseVariable,
isParameterPropertyDeclaration,
isParenthesizedExpression,
isParenthesizedTypeNode,
@@ -27481,7 +27480,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.Identifier:
if (!isThisInTypeQuery(node)) {
const symbol = getResolvedSymbol(node as Identifier);
return isConstantVariable(symbol) || isParameterOrCatchClauseVariable(symbol) && !isSymbolAssigned(symbol);
return isConstantVariable(symbol) || isParameterOrMutableLocalVariable(symbol) && !isSymbolAssigned(symbol);
}
break;
case SyntaxKind.PropertyAccessExpression:
@@ -28760,10 +28759,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// Check if a parameter or catch variable is assigned anywhere
function isSymbolAssigned(symbol: Symbol) {
if (!symbol.valueDeclaration) {
return !isPastLastAssignment(symbol, /*location*/ undefined);
}
// Return true if there are no assignments to the given symbol or if the given location
// is past the last assignment to the symbol.
function isPastLastAssignment(symbol: Symbol, location: Node | undefined) {
const parent = findAncestor(symbol.valueDeclaration, isFunctionOrSourceFile);
if (!parent) {
return false;
}
const parent = getRootDeclaration(symbol.valueDeclaration).parent;
const links = getNodeLinks(parent);
if (!(links.flags & NodeCheckFlags.AssignmentsMarked)) {
links.flags |= NodeCheckFlags.AssignmentsMarked;
@@ -28771,7 +28776,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
markNodeAssignments(parent);
}
}
return symbol.isAssigned || false;
return !symbol.lastAssignmentPos || location && symbol.lastAssignmentPos < location.pos;
}
// Check if a parameter or catch variable (or their bindings elements) is assigned anywhere
@@ -28789,27 +28794,98 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
function hasParentWithAssignmentsMarked(node: Node) {
return !!findAncestor(node.parent, node => (isFunctionLike(node) || isCatchClause(node)) && !!(getNodeLinks(node).flags & NodeCheckFlags.AssignmentsMarked));
return !!findAncestor(node.parent, node => isFunctionOrSourceFile(node) && !!(getNodeLinks(node).flags & NodeCheckFlags.AssignmentsMarked));
}
function isFunctionOrSourceFile(node: Node) {
return isFunctionLikeDeclaration(node) || isSourceFile(node);
}
// For all assignments within the given root node, record the last assignment source position for all
// referenced parameters and mutable local variables. When assignments occur in nested functions or
// references occur in export specifiers, record Number.MAX_VALUE as the assignment position. When
// assignments occur in compound statements, record the ending source position of the compound statement
// as the assignment position (this is more conservative than full control flow analysis, but requires
// only a single walk over the AST).
function markNodeAssignments(node: Node) {
if (node.kind === SyntaxKind.Identifier) {
if (isAssignmentTarget(node)) {
const symbol = getResolvedSymbol(node as Identifier);
if (isParameterOrCatchClauseVariable(symbol)) {
symbol.isAssigned = true;
switch (node.kind) {
case SyntaxKind.Identifier:
if (isAssignmentTarget(node)) {
const symbol = getResolvedSymbol(node as Identifier);
if (isParameterOrMutableLocalVariable(symbol) && symbol.lastAssignmentPos !== Number.MAX_VALUE) {
const referencingFunction = findAncestor(node, isFunctionOrSourceFile);
const declaringFunction = findAncestor(symbol.valueDeclaration, isFunctionOrSourceFile);
symbol.lastAssignmentPos = referencingFunction === declaringFunction ? extendAssignmentPosition(node, symbol.valueDeclaration!) : Number.MAX_VALUE;
}
}
return;
case SyntaxKind.ExportSpecifier:
const exportDeclaration = (node as ExportSpecifier).parent.parent;
if (!(node as ExportSpecifier).isTypeOnly && !exportDeclaration.isTypeOnly && !exportDeclaration.moduleSpecifier) {
const symbol = resolveEntityName((node as ExportSpecifier).propertyName || (node as ExportSpecifier).name, SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ true);
if (symbol && isParameterOrMutableLocalVariable(symbol)) {
symbol.lastAssignmentPos = Number.MAX_VALUE;
}
}
return;
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.TypeAliasDeclaration:
case SyntaxKind.EnumDeclaration:
return;
}
if (isTypeNode(node)) {
return;
}
forEachChild(node, markNodeAssignments);
}
// Extend the position of the given assignment target node to the end of any intervening variable statement,
// expression statement, compound statement, or class declaration occurring between the node and the given
// declaration node.
function extendAssignmentPosition(node: Node, declaration: Declaration) {
let pos = node.pos;
while (node && node.pos > declaration.pos) {
switch (node.kind) {
case SyntaxKind.VariableStatement:
case SyntaxKind.ExpressionStatement:
case SyntaxKind.IfStatement:
case SyntaxKind.DoStatement:
case SyntaxKind.WhileStatement:
case SyntaxKind.ForStatement:
case SyntaxKind.ForInStatement:
case SyntaxKind.ForOfStatement:
case SyntaxKind.WithStatement:
case SyntaxKind.SwitchStatement:
case SyntaxKind.TryStatement:
case SyntaxKind.ClassDeclaration:
pos = node.end;
}
node = node.parent;
}
else {
forEachChild(node, markNodeAssignments);
}
return pos;
}
function isConstantVariable(symbol: Symbol) {
return symbol.flags & SymbolFlags.Variable && (getDeclarationNodeFlagsFromSymbol(symbol) & NodeFlags.Constant) !== 0;
}
function isParameterOrMutableLocalVariable(symbol: Symbol) {
// Return true if symbol is a parameter, a catch clause variable, or a mutable local variable
const declaration = symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration);
return !!declaration && (
isParameter(declaration) ||
isVariableDeclaration(declaration) && (isCatchClause(declaration.parent) || isMutableLocalVariableDeclaration(declaration))
);
}
function isMutableLocalVariableDeclaration(declaration: VariableDeclaration) {
// Return true if symbol is a non-exported and non-global `let` variable
return !!(declaration.parent.flags & NodeFlags.Let) && !(
getCombinedModifierFlags(declaration) & ModifierFlags.Export ||
declaration.parent.parent.kind === SyntaxKind.VariableStatement && isGlobalSourceFile(declaration.parent.parent.parent)
);
}
function parameterInitializerContainsUndefined(declaration: ParameterDeclaration): boolean {
const links = getNodeLinks(declaration);
@@ -29160,13 +29236,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const isModuleExports = symbol.flags & SymbolFlags.ModuleExports;
const typeIsAutomatic = type === autoType || type === autoArrayType;
const isAutomaticTypeInNonNull = typeIsAutomatic && node.parent.kind === SyntaxKind.NonNullExpression;
// When the control flow originates in a function expression or arrow function and we are referencing
// a const variable or parameter from an outer function, we extend the origin of the control flow
// analysis to include the immediately enclosing function.
// When the control flow originates in a function expression, arrow function, method, or accessor, and
// we are referencing a closed-over const variable or parameter or mutable local variable past its last
// assignment, we extend the origin of the control flow analysis to include the immediately enclosing
// control flow container.
while (
flowContainer !== declarationContainer && (flowContainer.kind === SyntaxKind.FunctionExpression ||
flowContainer.kind === SyntaxKind.ArrowFunction || isObjectLiteralOrClassExpressionMethodOrAccessor(flowContainer)) &&
(isConstantVariable(localOrExportSymbol) && type !== autoArrayType || isParameter && !isSymbolAssigned(localOrExportSymbol))
flowContainer !== declarationContainer && (
flowContainer.kind === SyntaxKind.FunctionExpression ||
flowContainer.kind === SyntaxKind.ArrowFunction ||
isObjectLiteralOrClassExpressionMethodOrAccessor(flowContainer)
) && (
isConstantVariable(localOrExportSymbol) && type !== autoArrayType ||
isParameterOrMutableLocalVariable(localOrExportSymbol) && isPastLastAssignment(localOrExportSymbol, node)
)
) {
flowContainer = getControlFlowContainer(flowContainer);
}
+1 -1
View File
@@ -5825,8 +5825,8 @@ export interface Symbol {
/** @internal */ exportSymbol?: Symbol; // Exported symbol associated with this symbol
/** @internal */ constEnumOnlyModule: boolean | undefined; // True if module contains only const enums or other modules with only const enums
/** @internal */ isReferenced?: SymbolFlags; // True if the symbol is referenced elsewhere. Keeps track of the meaning of a reference in case a symbol is both a type parameter and parameter.
/** @internal */ lastAssignmentPos?: number; // Source position of last node that assigns value to symbol
/** @internal */ isReplaceableByMethod?: boolean; // Can this Javascript class property be replaced by a method symbol?
/** @internal */ isAssigned?: boolean; // True if the symbol is a parameter with assignments
/** @internal */ assignmentDeclarationMembers?: Map<number, Declaration>; // detected late-bound assignment declarations associated with the symbol
}
+1 -7
View File
@@ -8178,7 +8178,7 @@ function Symbol(this: Symbol, flags: SymbolFlags, name: __String) {
this.exportSymbol = undefined;
this.constEnumOnlyModule = undefined;
this.isReferenced = undefined;
this.isAssigned = undefined;
this.lastAssignmentPos = undefined;
(this as any).links = undefined; // used by TransientSymbol
}
@@ -10351,12 +10351,6 @@ export function isCatchClauseVariableDeclaration(node: Node) {
return node.kind === SyntaxKind.VariableDeclaration && node.parent.kind === SyntaxKind.CatchClause;
}
/** @internal */
export function isParameterOrCatchClauseVariable(symbol: Symbol) {
const declaration = symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration);
return !!declaration && (isParameter(declaration) || isCatchClauseVariableDeclaration(declaration));
}
/** @internal */
export function isFunctionExpressionOrArrowFunction(node: Node): node is FunctionExpression | ArrowFunction {
return node.kind === SyntaxKind.FunctionExpression || node.kind === SyntaxKind.ArrowFunction;