mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Preserve type refinements in closures created past last assignment (#56908)
This commit is contained in:
+102
-20
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user