mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Merge pull request #6553 from Microsoft/handleNestedBlockScopedName
handle block scoped binding in nested blocks
This commit is contained in:
+65
-55
@@ -7183,7 +7183,7 @@ namespace ts {
|
||||
|
||||
checkCollisionWithCapturedSuperVariable(node, node);
|
||||
checkCollisionWithCapturedThisVariable(node, node);
|
||||
checkBlockScopedBindingCapturedInLoop(node, symbol);
|
||||
checkNestedBlockScopedBinding(node, symbol);
|
||||
|
||||
return getNarrowedTypeOfSymbol(getExportSymbolOfValueSymbolIfExported(symbol), node);
|
||||
}
|
||||
@@ -7200,7 +7200,7 @@ namespace ts {
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkBlockScopedBindingCapturedInLoop(node: Identifier, symbol: Symbol): void {
|
||||
function checkNestedBlockScopedBinding(node: Identifier, symbol: Symbol): void {
|
||||
if (languageVersion >= ScriptTarget.ES6 ||
|
||||
(symbol.flags & (SymbolFlags.BlockScopedVariable | SymbolFlags.Class)) === 0 ||
|
||||
symbol.valueDeclaration.parent.kind === SyntaxKind.CatchClause) {
|
||||
@@ -7212,40 +7212,31 @@ namespace ts {
|
||||
// 2. walk from the declaration up to the boundary of lexical environment and check
|
||||
// if there is an iteration statement in between declaration and boundary (is binding/class declared inside iteration statement)
|
||||
|
||||
let container: Node;
|
||||
if (symbol.flags & SymbolFlags.Class) {
|
||||
// get parent of class declaration
|
||||
container = getClassLikeDeclarationOfSymbol(symbol).parent;
|
||||
}
|
||||
else {
|
||||
// nesting structure:
|
||||
// (variable declaration or binding element) -> variable declaration list -> container
|
||||
container = symbol.valueDeclaration;
|
||||
while (container.kind !== SyntaxKind.VariableDeclarationList) {
|
||||
container = container.parent;
|
||||
}
|
||||
// get the parent of variable declaration list
|
||||
container = container.parent;
|
||||
if (container.kind === SyntaxKind.VariableStatement) {
|
||||
// if parent is variable statement - get its parent
|
||||
container = container.parent;
|
||||
}
|
||||
}
|
||||
|
||||
const inFunction = isInsideFunction(node.parent, container);
|
||||
|
||||
const container = getEnclosingBlockScopeContainer(symbol.valueDeclaration);
|
||||
const usedInFunction = isInsideFunction(node.parent, container);
|
||||
let current = container;
|
||||
|
||||
let containedInIterationStatement = false;
|
||||
while (current && !nodeStartsNewLexicalEnvironment(current)) {
|
||||
if (isIterationStatement(current, /*lookInLabeledStatements*/ false)) {
|
||||
if (inFunction) {
|
||||
getNodeLinks(current).flags |= NodeCheckFlags.LoopWithBlockScopedBindingCapturedInFunction;
|
||||
}
|
||||
// mark value declaration so during emit they can have a special handling
|
||||
getNodeLinks(<VariableDeclaration>symbol.valueDeclaration).flags |= NodeCheckFlags.BlockScopedBindingInLoop;
|
||||
containedInIterationStatement = true;
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
if (containedInIterationStatement) {
|
||||
if (usedInFunction) {
|
||||
// mark iteration statement as containing block-scoped binding captured in some function
|
||||
getNodeLinks(current).flags |= NodeCheckFlags.LoopWithCapturedBlockScopedBinding;
|
||||
}
|
||||
// set 'declared inside loop' bit on the block-scoped binding
|
||||
getNodeLinks(symbol.valueDeclaration).flags |= NodeCheckFlags.BlockScopedBindingInLoop;
|
||||
}
|
||||
|
||||
if (usedInFunction) {
|
||||
getNodeLinks(symbol.valueDeclaration).flags |= NodeCheckFlags.CapturedBlockScopedBinding;
|
||||
}
|
||||
}
|
||||
|
||||
function captureLexicalThis(node: Node, container: Node): void {
|
||||
@@ -15652,42 +15643,61 @@ namespace ts {
|
||||
return symbol && symbol.flags & SymbolFlags.Alias ? getDeclarationOfAliasSymbol(symbol) : undefined;
|
||||
}
|
||||
|
||||
function isStatementWithLocals(node: Node) {
|
||||
switch (node.kind) {
|
||||
case SyntaxKind.Block:
|
||||
case SyntaxKind.CaseBlock:
|
||||
case SyntaxKind.ForStatement:
|
||||
case SyntaxKind.ForInStatement:
|
||||
case SyntaxKind.ForOfStatement:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isNestedRedeclarationSymbol(symbol: Symbol): boolean {
|
||||
function isSymbolOfDeclarationWithCollidingName(symbol: Symbol): boolean {
|
||||
if (symbol.flags & SymbolFlags.BlockScoped) {
|
||||
const links = getSymbolLinks(symbol);
|
||||
if (links.isNestedRedeclaration === undefined) {
|
||||
if (links.isDeclaratonWithCollidingName === undefined) {
|
||||
const container = getEnclosingBlockScopeContainer(symbol.valueDeclaration);
|
||||
links.isNestedRedeclaration = isStatementWithLocals(container) &&
|
||||
!!resolveName(container.parent, symbol.name, SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined);
|
||||
if (isStatementWithLocals(container)) {
|
||||
const nodeLinks = getNodeLinks(symbol.valueDeclaration);
|
||||
if (!!resolveName(container.parent, symbol.name, SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined)) {
|
||||
// redeclaration - always should be renamed
|
||||
links.isDeclaratonWithCollidingName = true;
|
||||
}
|
||||
else if (nodeLinks.flags & NodeCheckFlags.CapturedBlockScopedBinding) {
|
||||
// binding is captured in the function
|
||||
// should be renamed if:
|
||||
// - binding is not top level - top level bindings never collide with anything
|
||||
// AND
|
||||
// - binding is not declared in loop, should be renamed to avoid name reuse across siblings
|
||||
// let a, b
|
||||
// { let x = 1; a = () => x; }
|
||||
// { let x = 100; b = () => x; }
|
||||
// console.log(a()); // should print '1'
|
||||
// console.log(b()); // should print '100'
|
||||
// OR
|
||||
// - binding is declared inside loop but not in inside initializer of iteration statement or directly inside loop body
|
||||
// * variables from initializer are passed to rewritted loop body as parameters so they are not captured directly
|
||||
// * variables that are declared immediately in loop body will become top level variable after loop is rewritten and thus
|
||||
// they will not collide with anything
|
||||
const isDeclaredInLoop = nodeLinks.flags & NodeCheckFlags.BlockScopedBindingInLoop;
|
||||
const inLoopInitializer = isIterationStatement(container, /*lookInLabeledStatements*/ false);
|
||||
const inLoopBodyBlock = container.kind === SyntaxKind.Block && isIterationStatement(container.parent, /*lookInLabeledStatements*/ false);
|
||||
|
||||
links.isDeclaratonWithCollidingName = !isBlockScopedContainerTopLevel(container) && (!isDeclaredInLoop || (!inLoopInitializer && !inLoopBodyBlock));
|
||||
}
|
||||
else {
|
||||
links.isDeclaratonWithCollidingName = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return links.isNestedRedeclaration;
|
||||
return links.isDeclaratonWithCollidingName;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// When resolved as an expression identifier, if the given node references a nested block scoped entity with
|
||||
// a name that hides an existing name, return the declaration of that entity. Otherwise, return undefined.
|
||||
function getReferencedNestedRedeclaration(node: Identifier): Declaration {
|
||||
// a name that either hides an existing name or might hide it when compiled downlevel,
|
||||
// return the declaration of that entity. Otherwise, return undefined.
|
||||
function getReferencedDeclarationWithCollidingName(node: Identifier): Declaration {
|
||||
const symbol = getReferencedValueSymbol(node);
|
||||
return symbol && isNestedRedeclarationSymbol(symbol) ? symbol.valueDeclaration : undefined;
|
||||
return symbol && isSymbolOfDeclarationWithCollidingName(symbol) ? symbol.valueDeclaration : undefined;
|
||||
}
|
||||
|
||||
// Return true if the given node is a declaration of a nested block scoped entity with a name that hides an
|
||||
// existing name.
|
||||
function isNestedRedeclaration(node: Declaration): boolean {
|
||||
return isNestedRedeclarationSymbol(getSymbolOfNode(node));
|
||||
// Return true if the given node is a declaration of a nested block scoped entity with a name that either hides an
|
||||
// existing name or might hide a name when compiled downlevel
|
||||
function isDeclarationWithCollidingName(node: Declaration): boolean {
|
||||
return isSymbolOfDeclarationWithCollidingName(getSymbolOfNode(node));
|
||||
}
|
||||
|
||||
function isValueAliasDeclaration(node: Node): boolean {
|
||||
@@ -15888,8 +15898,8 @@ namespace ts {
|
||||
return {
|
||||
getReferencedExportContainer,
|
||||
getReferencedImportDeclaration,
|
||||
getReferencedNestedRedeclaration,
|
||||
isNestedRedeclaration,
|
||||
getReferencedDeclarationWithCollidingName,
|
||||
isDeclarationWithCollidingName,
|
||||
isValueAliasDeclaration,
|
||||
hasGlobalName,
|
||||
isReferencedAliasDeclaration,
|
||||
|
||||
+54
-28
@@ -1530,7 +1530,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||
}
|
||||
|
||||
if (languageVersion !== ScriptTarget.ES6) {
|
||||
const declaration = resolver.getReferencedNestedRedeclaration(node);
|
||||
const declaration = resolver.getReferencedDeclarationWithCollidingName(node);
|
||||
if (declaration) {
|
||||
write(getGeneratedNameForNode(declaration.name));
|
||||
return;
|
||||
@@ -1546,7 +1546,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||
}
|
||||
}
|
||||
|
||||
function isNameOfNestedRedeclaration(node: Identifier) {
|
||||
function isNameOfNestedBlockScopedRedeclarationOrCapturedBinding(node: Identifier) {
|
||||
if (languageVersion < ScriptTarget.ES6) {
|
||||
const parent = node.parent;
|
||||
switch (parent.kind) {
|
||||
@@ -1554,7 +1554,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||
case SyntaxKind.ClassDeclaration:
|
||||
case SyntaxKind.EnumDeclaration:
|
||||
case SyntaxKind.VariableDeclaration:
|
||||
return (<Declaration>parent).name === node && resolver.isNestedRedeclaration(<Declaration>parent);
|
||||
return (<Declaration>parent).name === node && resolver.isDeclarationWithCollidingName(<Declaration>parent);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -1576,7 +1576,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||
else if (isExpressionIdentifier(node)) {
|
||||
emitExpressionIdentifier(node);
|
||||
}
|
||||
else if (isNameOfNestedRedeclaration(node)) {
|
||||
else if (isNameOfNestedBlockScopedRedeclarationOrCapturedBinding(node)) {
|
||||
write(getGeneratedNameForNode(node));
|
||||
}
|
||||
else if (nodeIsSynthesized(node)) {
|
||||
@@ -2891,7 +2891,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||
|
||||
function shouldConvertLoopBody(node: IterationStatement): boolean {
|
||||
return languageVersion < ScriptTarget.ES6 &&
|
||||
(resolver.getNodeCheckFlags(node) & NodeCheckFlags.LoopWithBlockScopedBindingCapturedInFunction) !== 0;
|
||||
(resolver.getNodeCheckFlags(node) & NodeCheckFlags.LoopWithCapturedBlockScopedBinding) !== 0;
|
||||
}
|
||||
|
||||
function emitLoop(node: IterationStatement, loopEmitter: (n: IterationStatement, convertedLoop: ConvertedLoop) => void): void {
|
||||
@@ -3045,7 +3045,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||
|
||||
function collectNames(name: Identifier | BindingPattern): void {
|
||||
if (name.kind === SyntaxKind.Identifier) {
|
||||
const nameText = isNameOfNestedRedeclaration(<Identifier>name) ? getGeneratedNameForNode(name) : (<Identifier>name).text;
|
||||
const nameText = isNameOfNestedBlockScopedRedeclarationOrCapturedBinding(<Identifier>name) ? getGeneratedNameForNode(name) : (<Identifier>name).text;
|
||||
loopParameters.push(nameText);
|
||||
}
|
||||
else {
|
||||
@@ -4065,22 +4065,56 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||
}
|
||||
else {
|
||||
let initializer = node.initializer;
|
||||
if (!initializer && languageVersion < ScriptTarget.ES6) {
|
||||
if (!initializer &&
|
||||
languageVersion < ScriptTarget.ES6 &&
|
||||
// for names - binding patterns that lack initializer there is no point to emit explicit initializer
|
||||
// since downlevel codegen for destructuring will fail in the absence of initializer so all binding elements will say uninitialized
|
||||
node.name.kind === SyntaxKind.Identifier) {
|
||||
|
||||
// downlevel emit for non-initialized let bindings defined in loops
|
||||
// for (...) { let x; }
|
||||
// should be
|
||||
// for (...) { var <some-uniqie-name> = void 0; }
|
||||
// this is necessary to preserve ES6 semantic in scenarios like
|
||||
// for (...) { let x; console.log(x); x = 1 } // assignment on one iteration should not affect other iterations
|
||||
const isLetDefinedInLoop =
|
||||
(resolver.getNodeCheckFlags(node) & NodeCheckFlags.BlockScopedBindingInLoop) &&
|
||||
(getCombinedFlagsForIdentifier(<Identifier>node.name) & NodeFlags.Let);
|
||||
const container = getEnclosingBlockScopeContainer(node);
|
||||
const flags = resolver.getNodeCheckFlags(node);
|
||||
|
||||
// NOTE: default initialization should not be added to let bindings in for-in\for-of statements
|
||||
if (isLetDefinedInLoop &&
|
||||
node.parent.parent.kind !== SyntaxKind.ForInStatement &&
|
||||
node.parent.parent.kind !== SyntaxKind.ForOfStatement) {
|
||||
// nested let bindings might need to be initialized explicitly to preserve ES6 semantic
|
||||
// { let x = 1; }
|
||||
// { let x; } // x here should be undefined. not 1
|
||||
// NOTES:
|
||||
// Top level bindings never collide with anything and thus don't require explicit initialization.
|
||||
// As for nested let bindings there are two cases:
|
||||
// - nested let bindings that were not renamed definitely should be initialized explicitly
|
||||
// { let x = 1; }
|
||||
// { let x; if (some-condition) { x = 1}; if (x) { /*1*/ } }
|
||||
// Without explicit initialization code in /*1*/ can be executed even if some-condition is evaluated to false
|
||||
// - renaming introduces fresh name that should not collide with any existing names, however renamed bindings sometimes also should be
|
||||
// explicitly initialized. One particular case: non-captured binding declared inside loop body (but not in loop initializer)
|
||||
// let x;
|
||||
// for (;;) {
|
||||
// let x;
|
||||
// }
|
||||
// in downlevel codegen inner 'x' will be renamed so it won't collide with outer 'x' however it will should be reset on every iteration
|
||||
// as if it was declared anew.
|
||||
// * Why non-captured binding - because if loop contains block scoped binding captured in some function then loop body will be rewritten
|
||||
// to have a fresh scope on every iteration so everything will just work.
|
||||
// * Why loop initializer is excluded - since we've introduced a fresh name it already will be undefined.
|
||||
const isCapturedInFunction = flags & NodeCheckFlags.CapturedBlockScopedBinding;
|
||||
const isDeclaredInLoop = flags & NodeCheckFlags.BlockScopedBindingInLoop;
|
||||
|
||||
const emittedAsTopLevel =
|
||||
isBlockScopedContainerTopLevel(container) ||
|
||||
(isCapturedInFunction && isDeclaredInLoop && container.kind === SyntaxKind.Block && isIterationStatement(container.parent, /*lookInLabeledStatements*/ false));
|
||||
|
||||
const emittedAsNestedLetDeclaration =
|
||||
getCombinedNodeFlags(node) & NodeFlags.Let &&
|
||||
!emittedAsTopLevel;
|
||||
|
||||
const emitExplicitInitializer =
|
||||
emittedAsNestedLetDeclaration &&
|
||||
container.kind !== SyntaxKind.ForInStatement &&
|
||||
container.kind !== SyntaxKind.ForOfStatement &&
|
||||
(
|
||||
!resolver.isDeclarationWithCollidingName(node) ||
|
||||
(isDeclaredInLoop && !isCapturedInFunction && !isIterationStatement(container, /*lookInLabeledStatements*/ false))
|
||||
);
|
||||
if (emitExplicitInitializer) {
|
||||
initializer = createVoidZero();
|
||||
}
|
||||
}
|
||||
@@ -4115,14 +4149,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||
}
|
||||
}
|
||||
|
||||
function getCombinedFlagsForIdentifier(node: Identifier): NodeFlags {
|
||||
if (!node.parent || (node.parent.kind !== SyntaxKind.VariableDeclaration && node.parent.kind !== SyntaxKind.BindingElement)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return getCombinedNodeFlags(node.parent);
|
||||
}
|
||||
|
||||
function isES6ExportedDeclaration(node: Node) {
|
||||
return !!(node.flags & NodeFlags.Export) &&
|
||||
modulekind === ModuleKind.ES6 &&
|
||||
|
||||
@@ -1903,8 +1903,8 @@ namespace ts {
|
||||
hasGlobalName(name: string): boolean;
|
||||
getReferencedExportContainer(node: Identifier): SourceFile | ModuleDeclaration | EnumDeclaration;
|
||||
getReferencedImportDeclaration(node: Identifier): Declaration;
|
||||
getReferencedNestedRedeclaration(node: Identifier): Declaration;
|
||||
isNestedRedeclaration(node: Declaration): boolean;
|
||||
getReferencedDeclarationWithCollidingName(node: Identifier): Declaration;
|
||||
isDeclarationWithCollidingName(node: Declaration): boolean;
|
||||
isValueAliasDeclaration(node: Node): boolean;
|
||||
isReferencedAliasDeclaration(node: Node, checkChildren?: boolean): boolean;
|
||||
isTopLevelValueImportEqualsWithEntityName(node: ImportEqualsDeclaration): boolean;
|
||||
@@ -2040,7 +2040,7 @@ namespace ts {
|
||||
containingType?: UnionOrIntersectionType; // Containing union or intersection type for synthetic property
|
||||
resolvedExports?: SymbolTable; // Resolved exports of module
|
||||
exportsChecked?: boolean; // True if exports of external module have been checked
|
||||
isNestedRedeclaration?: boolean; // True if symbol is block scoped redeclaration
|
||||
isDeclaratonWithCollidingName?: boolean; // True if symbol is block scoped redeclaration
|
||||
bindingElement?: BindingElement; // Binding element associated with property symbol
|
||||
exportsSomeValue?: boolean; // true if module exports some value (not just types)
|
||||
}
|
||||
@@ -2066,9 +2066,10 @@ namespace ts {
|
||||
|
||||
// Values for enum members have been computed, and any errors have been reported for them.
|
||||
EnumValuesComputed = 0x00004000,
|
||||
BlockScopedBindingInLoop = 0x00008000,
|
||||
LexicalModuleMergesWithClass = 0x00010000, // Instantiated lexical module declaration is merged with a previous class declaration.
|
||||
LoopWithBlockScopedBindingCapturedInFunction = 0x00020000, // Loop that contains block scoped variable captured in closure
|
||||
LexicalModuleMergesWithClass = 0x00008000, // Instantiated lexical module declaration is merged with a previous class declaration.
|
||||
LoopWithCapturedBlockScopedBinding = 0x00010000, // Loop that contains block scoped variable captured in closure
|
||||
CapturedBlockScopedBinding = 0x00020000, // Block-scoped binding that is captured in some function
|
||||
BlockScopedBindingInLoop = 0x00040000, // Block-scoped binding with declaration nested inside iteration statement
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
|
||||
@@ -151,6 +151,18 @@ namespace ts {
|
||||
return <SourceFile>node;
|
||||
}
|
||||
|
||||
export function isStatementWithLocals(node: Node) {
|
||||
switch (node.kind) {
|
||||
case SyntaxKind.Block:
|
||||
case SyntaxKind.CaseBlock:
|
||||
case SyntaxKind.ForStatement:
|
||||
case SyntaxKind.ForInStatement:
|
||||
case SyntaxKind.ForOfStatement:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getStartPositionOfLine(line: number, sourceFile: SourceFile): number {
|
||||
Debug.assert(line >= 0);
|
||||
return getLineStarts(sourceFile)[line];
|
||||
@@ -256,6 +268,13 @@ namespace ts {
|
||||
((<ModuleDeclaration>node).name.kind === SyntaxKind.StringLiteral || isGlobalScopeAugmentation(<ModuleDeclaration>node));
|
||||
}
|
||||
|
||||
export function isBlockScopedContainerTopLevel(node: Node): boolean {
|
||||
return node.kind === SyntaxKind.SourceFile ||
|
||||
node.kind === SyntaxKind.ModuleDeclaration ||
|
||||
isFunctionLike(node) ||
|
||||
isFunctionBlock(node);
|
||||
}
|
||||
|
||||
export function isGlobalScopeAugmentation(module: ModuleDeclaration): boolean {
|
||||
return !!(module.flags & NodeFlags.GlobalAugmentation);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user