Skip to content

Commit 2b22a47

Browse files
committed
Merge pull request #6553 from Microsoft/handleNestedBlockScopedName
handle block scoped binding in nested blocks
2 parents 98603f9 + e67ff39 commit 2b22a47

File tree

78 files changed

+4638
-123
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+4638
-123
lines changed

src/compiler/checker.ts

Lines changed: 65 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7183,7 +7183,7 @@ namespace ts {
71837183

71847184
checkCollisionWithCapturedSuperVariable(node, node);
71857185
checkCollisionWithCapturedThisVariable(node, node);
7186-
checkBlockScopedBindingCapturedInLoop(node, symbol);
7186+
checkNestedBlockScopedBinding(node, symbol);
71877187

71887188
return getNarrowedTypeOfSymbol(getExportSymbolOfValueSymbolIfExported(symbol), node);
71897189
}
@@ -7200,7 +7200,7 @@ namespace ts {
72007200
return false;
72017201
}
72027202

7203-
function checkBlockScopedBindingCapturedInLoop(node: Identifier, symbol: Symbol): void {
7203+
function checkNestedBlockScopedBinding(node: Identifier, symbol: Symbol): void {
72047204
if (languageVersion >= ScriptTarget.ES6 ||
72057205
(symbol.flags & (SymbolFlags.BlockScopedVariable | SymbolFlags.Class)) === 0 ||
72067206
symbol.valueDeclaration.parent.kind === SyntaxKind.CatchClause) {
@@ -7212,40 +7212,31 @@ namespace ts {
72127212
// 2. walk from the declaration up to the boundary of lexical environment and check
72137213
// if there is an iteration statement in between declaration and boundary (is binding/class declared inside iteration statement)
72147214

7215-
let container: Node;
7216-
if (symbol.flags & SymbolFlags.Class) {
7217-
// get parent of class declaration
7218-
container = getClassLikeDeclarationOfSymbol(symbol).parent;
7219-
}
7220-
else {
7221-
// nesting structure:
7222-
// (variable declaration or binding element) -> variable declaration list -> container
7223-
container = symbol.valueDeclaration;
7224-
while (container.kind !== SyntaxKind.VariableDeclarationList) {
7225-
container = container.parent;
7226-
}
7227-
// get the parent of variable declaration list
7228-
container = container.parent;
7229-
if (container.kind === SyntaxKind.VariableStatement) {
7230-
// if parent is variable statement - get its parent
7231-
container = container.parent;
7232-
}
7233-
}
7234-
7235-
const inFunction = isInsideFunction(node.parent, container);
7236-
7215+
const container = getEnclosingBlockScopeContainer(symbol.valueDeclaration);
7216+
const usedInFunction = isInsideFunction(node.parent, container);
72377217
let current = container;
7218+
7219+
let containedInIterationStatement = false;
72387220
while (current && !nodeStartsNewLexicalEnvironment(current)) {
72397221
if (isIterationStatement(current, /*lookInLabeledStatements*/ false)) {
7240-
if (inFunction) {
7241-
getNodeLinks(current).flags |= NodeCheckFlags.LoopWithBlockScopedBindingCapturedInFunction;
7242-
}
7243-
// mark value declaration so during emit they can have a special handling
7244-
getNodeLinks(<VariableDeclaration>symbol.valueDeclaration).flags |= NodeCheckFlags.BlockScopedBindingInLoop;
7222+
containedInIterationStatement = true;
72457223
break;
72467224
}
72477225
current = current.parent;
72487226
}
7227+
7228+
if (containedInIterationStatement) {
7229+
if (usedInFunction) {
7230+
// mark iteration statement as containing block-scoped binding captured in some function
7231+
getNodeLinks(current).flags |= NodeCheckFlags.LoopWithCapturedBlockScopedBinding;
7232+
}
7233+
// set 'declared inside loop' bit on the block-scoped binding
7234+
getNodeLinks(symbol.valueDeclaration).flags |= NodeCheckFlags.BlockScopedBindingInLoop;
7235+
}
7236+
7237+
if (usedInFunction) {
7238+
getNodeLinks(symbol.valueDeclaration).flags |= NodeCheckFlags.CapturedBlockScopedBinding;
7239+
}
72497240
}
72507241

72517242
function captureLexicalThis(node: Node, container: Node): void {
@@ -15652,42 +15643,61 @@ namespace ts {
1565215643
return symbol && symbol.flags & SymbolFlags.Alias ? getDeclarationOfAliasSymbol(symbol) : undefined;
1565315644
}
1565415645

15655-
function isStatementWithLocals(node: Node) {
15656-
switch (node.kind) {
15657-
case SyntaxKind.Block:
15658-
case SyntaxKind.CaseBlock:
15659-
case SyntaxKind.ForStatement:
15660-
case SyntaxKind.ForInStatement:
15661-
case SyntaxKind.ForOfStatement:
15662-
return true;
15663-
}
15664-
return false;
15665-
}
15666-
15667-
function isNestedRedeclarationSymbol(symbol: Symbol): boolean {
15646+
function isSymbolOfDeclarationWithCollidingName(symbol: Symbol): boolean {
1566815647
if (symbol.flags & SymbolFlags.BlockScoped) {
1566915648
const links = getSymbolLinks(symbol);
15670-
if (links.isNestedRedeclaration === undefined) {
15649+
if (links.isDeclaratonWithCollidingName === undefined) {
1567115650
const container = getEnclosingBlockScopeContainer(symbol.valueDeclaration);
15672-
links.isNestedRedeclaration = isStatementWithLocals(container) &&
15673-
!!resolveName(container.parent, symbol.name, SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined);
15651+
if (isStatementWithLocals(container)) {
15652+
const nodeLinks = getNodeLinks(symbol.valueDeclaration);
15653+
if (!!resolveName(container.parent, symbol.name, SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined)) {
15654+
// redeclaration - always should be renamed
15655+
links.isDeclaratonWithCollidingName = true;
15656+
}
15657+
else if (nodeLinks.flags & NodeCheckFlags.CapturedBlockScopedBinding) {
15658+
// binding is captured in the function
15659+
// should be renamed if:
15660+
// - binding is not top level - top level bindings never collide with anything
15661+
// AND
15662+
// - binding is not declared in loop, should be renamed to avoid name reuse across siblings
15663+
// let a, b
15664+
// { let x = 1; a = () => x; }
15665+
// { let x = 100; b = () => x; }
15666+
// console.log(a()); // should print '1'
15667+
// console.log(b()); // should print '100'
15668+
// OR
15669+
// - binding is declared inside loop but not in inside initializer of iteration statement or directly inside loop body
15670+
// * variables from initializer are passed to rewritted loop body as parameters so they are not captured directly
15671+
// * variables that are declared immediately in loop body will become top level variable after loop is rewritten and thus
15672+
// they will not collide with anything
15673+
const isDeclaredInLoop = nodeLinks.flags & NodeCheckFlags.BlockScopedBindingInLoop;
15674+
const inLoopInitializer = isIterationStatement(container, /*lookInLabeledStatements*/ false);
15675+
const inLoopBodyBlock = container.kind === SyntaxKind.Block && isIterationStatement(container.parent, /*lookInLabeledStatements*/ false);
15676+
15677+
links.isDeclaratonWithCollidingName = !isBlockScopedContainerTopLevel(container) && (!isDeclaredInLoop || (!inLoopInitializer && !inLoopBodyBlock));
15678+
}
15679+
else {
15680+
links.isDeclaratonWithCollidingName = false;
15681+
}
15682+
}
1567415683
}
15675-
return links.isNestedRedeclaration;
15684+
return links.isDeclaratonWithCollidingName;
1567615685
}
1567715686
return false;
1567815687
}
1567915688

1568015689
// When resolved as an expression identifier, if the given node references a nested block scoped entity with
15681-
// a name that hides an existing name, return the declaration of that entity. Otherwise, return undefined.
15682-
function getReferencedNestedRedeclaration(node: Identifier): Declaration {
15690+
// a name that either hides an existing name or might hide it when compiled downlevel,
15691+
// return the declaration of that entity. Otherwise, return undefined.
15692+
function getReferencedDeclarationWithCollidingName(node: Identifier): Declaration {
1568315693
const symbol = getReferencedValueSymbol(node);
15684-
return symbol && isNestedRedeclarationSymbol(symbol) ? symbol.valueDeclaration : undefined;
15694+
return symbol && isSymbolOfDeclarationWithCollidingName(symbol) ? symbol.valueDeclaration : undefined;
1568515695
}
1568615696

15687-
// Return true if the given node is a declaration of a nested block scoped entity with a name that hides an
15688-
// existing name.
15689-
function isNestedRedeclaration(node: Declaration): boolean {
15690-
return isNestedRedeclarationSymbol(getSymbolOfNode(node));
15697+
// Return true if the given node is a declaration of a nested block scoped entity with a name that either hides an
15698+
// existing name or might hide a name when compiled downlevel
15699+
function isDeclarationWithCollidingName(node: Declaration): boolean {
15700+
return isSymbolOfDeclarationWithCollidingName(getSymbolOfNode(node));
1569115701
}
1569215702

1569315703
function isValueAliasDeclaration(node: Node): boolean {
@@ -15888,8 +15898,8 @@ namespace ts {
1588815898
return {
1588915899
getReferencedExportContainer,
1589015900
getReferencedImportDeclaration,
15891-
getReferencedNestedRedeclaration,
15892-
isNestedRedeclaration,
15901+
getReferencedDeclarationWithCollidingName,
15902+
isDeclarationWithCollidingName,
1589315903
isValueAliasDeclaration,
1589415904
hasGlobalName,
1589515905
isReferencedAliasDeclaration,

src/compiler/emitter.ts

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,7 +1530,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
15301530
}
15311531

15321532
if (languageVersion !== ScriptTarget.ES6) {
1533-
const declaration = resolver.getReferencedNestedRedeclaration(node);
1533+
const declaration = resolver.getReferencedDeclarationWithCollidingName(node);
15341534
if (declaration) {
15351535
write(getGeneratedNameForNode(declaration.name));
15361536
return;
@@ -1546,15 +1546,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
15461546
}
15471547
}
15481548

1549-
function isNameOfNestedRedeclaration(node: Identifier) {
1549+
function isNameOfNestedBlockScopedRedeclarationOrCapturedBinding(node: Identifier) {
15501550
if (languageVersion < ScriptTarget.ES6) {
15511551
const parent = node.parent;
15521552
switch (parent.kind) {
15531553
case SyntaxKind.BindingElement:
15541554
case SyntaxKind.ClassDeclaration:
15551555
case SyntaxKind.EnumDeclaration:
15561556
case SyntaxKind.VariableDeclaration:
1557-
return (<Declaration>parent).name === node && resolver.isNestedRedeclaration(<Declaration>parent);
1557+
return (<Declaration>parent).name === node && resolver.isDeclarationWithCollidingName(<Declaration>parent);
15581558
}
15591559
}
15601560
return false;
@@ -1576,7 +1576,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
15761576
else if (isExpressionIdentifier(node)) {
15771577
emitExpressionIdentifier(node);
15781578
}
1579-
else if (isNameOfNestedRedeclaration(node)) {
1579+
else if (isNameOfNestedBlockScopedRedeclarationOrCapturedBinding(node)) {
15801580
write(getGeneratedNameForNode(node));
15811581
}
15821582
else if (nodeIsSynthesized(node)) {
@@ -2891,7 +2891,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
28912891

28922892
function shouldConvertLoopBody(node: IterationStatement): boolean {
28932893
return languageVersion < ScriptTarget.ES6 &&
2894-
(resolver.getNodeCheckFlags(node) & NodeCheckFlags.LoopWithBlockScopedBindingCapturedInFunction) !== 0;
2894+
(resolver.getNodeCheckFlags(node) & NodeCheckFlags.LoopWithCapturedBlockScopedBinding) !== 0;
28952895
}
28962896

28972897
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
30453045

30463046
function collectNames(name: Identifier | BindingPattern): void {
30473047
if (name.kind === SyntaxKind.Identifier) {
3048-
const nameText = isNameOfNestedRedeclaration(<Identifier>name) ? getGeneratedNameForNode(name) : (<Identifier>name).text;
3048+
const nameText = isNameOfNestedBlockScopedRedeclarationOrCapturedBinding(<Identifier>name) ? getGeneratedNameForNode(name) : (<Identifier>name).text;
30493049
loopParameters.push(nameText);
30503050
}
30513051
else {
@@ -4065,22 +4065,56 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
40654065
}
40664066
else {
40674067
let initializer = node.initializer;
4068-
if (!initializer && languageVersion < ScriptTarget.ES6) {
4069-
4070-
// downlevel emit for non-initialized let bindings defined in loops
4071-
// for (...) { let x; }
4072-
// should be
4073-
// for (...) { var <some-uniqie-name> = void 0; }
4074-
// this is necessary to preserve ES6 semantic in scenarios like
4075-
// for (...) { let x; console.log(x); x = 1 } // assignment on one iteration should not affect other iterations
4076-
const isLetDefinedInLoop =
4077-
(resolver.getNodeCheckFlags(node) & NodeCheckFlags.BlockScopedBindingInLoop) &&
4078-
(getCombinedFlagsForIdentifier(<Identifier>node.name) & NodeFlags.Let);
4079-
4080-
// NOTE: default initialization should not be added to let bindings in for-in\for-of statements
4081-
if (isLetDefinedInLoop &&
4082-
node.parent.parent.kind !== SyntaxKind.ForInStatement &&
4083-
node.parent.parent.kind !== SyntaxKind.ForOfStatement) {
4068+
if (!initializer &&
4069+
languageVersion < ScriptTarget.ES6 &&
4070+
// for names - binding patterns that lack initializer there is no point to emit explicit initializer
4071+
// since downlevel codegen for destructuring will fail in the absence of initializer so all binding elements will say uninitialized
4072+
node.name.kind === SyntaxKind.Identifier) {
4073+
4074+
const container = getEnclosingBlockScopeContainer(node);
4075+
const flags = resolver.getNodeCheckFlags(node);
4076+
4077+
// nested let bindings might need to be initialized explicitly to preserve ES6 semantic
4078+
// { let x = 1; }
4079+
// { let x; } // x here should be undefined. not 1
4080+
// NOTES:
4081+
// Top level bindings never collide with anything and thus don't require explicit initialization.
4082+
// As for nested let bindings there are two cases:
4083+
// - nested let bindings that were not renamed definitely should be initialized explicitly
4084+
// { let x = 1; }
4085+
// { let x; if (some-condition) { x = 1}; if (x) { /*1*/ } }
4086+
// Without explicit initialization code in /*1*/ can be executed even if some-condition is evaluated to false
4087+
// - renaming introduces fresh name that should not collide with any existing names, however renamed bindings sometimes also should be
4088+
// explicitly initialized. One particular case: non-captured binding declared inside loop body (but not in loop initializer)
4089+
// let x;
4090+
// for (;;) {
4091+
// let x;
4092+
// }
4093+
// 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
4094+
// as if it was declared anew.
4095+
// * Why non-captured binding - because if loop contains block scoped binding captured in some function then loop body will be rewritten
4096+
// to have a fresh scope on every iteration so everything will just work.
4097+
// * Why loop initializer is excluded - since we've introduced a fresh name it already will be undefined.
4098+
const isCapturedInFunction = flags & NodeCheckFlags.CapturedBlockScopedBinding;
4099+
const isDeclaredInLoop = flags & NodeCheckFlags.BlockScopedBindingInLoop;
4100+
4101+
const emittedAsTopLevel =
4102+
isBlockScopedContainerTopLevel(container) ||
4103+
(isCapturedInFunction && isDeclaredInLoop && container.kind === SyntaxKind.Block && isIterationStatement(container.parent, /*lookInLabeledStatements*/ false));
4104+
4105+
const emittedAsNestedLetDeclaration =
4106+
getCombinedNodeFlags(node) & NodeFlags.Let &&
4107+
!emittedAsTopLevel;
4108+
4109+
const emitExplicitInitializer =
4110+
emittedAsNestedLetDeclaration &&
4111+
container.kind !== SyntaxKind.ForInStatement &&
4112+
container.kind !== SyntaxKind.ForOfStatement &&
4113+
(
4114+
!resolver.isDeclarationWithCollidingName(node) ||
4115+
(isDeclaredInLoop && !isCapturedInFunction && !isIterationStatement(container, /*lookInLabeledStatements*/ false))
4116+
);
4117+
if (emitExplicitInitializer) {
40844118
initializer = createVoidZero();
40854119
}
40864120
}
@@ -4115,14 +4149,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
41154149
}
41164150
}
41174151

4118-
function getCombinedFlagsForIdentifier(node: Identifier): NodeFlags {
4119-
if (!node.parent || (node.parent.kind !== SyntaxKind.VariableDeclaration && node.parent.kind !== SyntaxKind.BindingElement)) {
4120-
return 0;
4121-
}
4122-
4123-
return getCombinedNodeFlags(node.parent);
4124-
}
4125-
41264152
function isES6ExportedDeclaration(node: Node) {
41274153
return !!(node.flags & NodeFlags.Export) &&
41284154
modulekind === ModuleKind.ES6 &&

0 commit comments

Comments
 (0)