Skip to content

Commit b351223

Browse files
committed
Fix property name bindings for class expr in loops
1 parent cf24c4f commit b351223

18 files changed

+401
-27
lines changed

src/compiler/checker.ts

+31-12
Original file line numberDiff line numberDiff line change
@@ -20722,6 +20722,10 @@ namespace ts {
2072220722
return findAncestor(node, n => n === container ? "quit" : n === container.initializer || n === container.condition || n === container.incrementor || n === container.statement);
2072320723
}
2072420724

20725+
function getEnclosingIterationStatement(node: Node): Node | undefined {
20726+
return findAncestor(node, n => (!n || nodeStartsNewLexicalEnvironment(n)) ? "quit" : isIterationStatement(n, /*lookInLabeledStatements*/ false));
20727+
}
20728+
2072520729
function checkNestedBlockScopedBinding(node: Identifier, symbol: Symbol): void {
2072620730
if (languageVersion >= ScriptTarget.ES2015 ||
2072720731
(symbol.flags & (SymbolFlags.BlockScopedVariable | SymbolFlags.Class)) === 0 ||
@@ -20737,18 +20741,9 @@ namespace ts {
2073720741

2073820742
const container = getEnclosingBlockScopeContainer(symbol.valueDeclaration);
2073920743
const usedInFunction = isInsideFunction(node.parent, container);
20740-
let current = container;
20741-
20742-
let containedInIterationStatement = false;
20743-
while (current && !nodeStartsNewLexicalEnvironment(current)) {
20744-
if (isIterationStatement(current, /*lookInLabeledStatements*/ false)) {
20745-
containedInIterationStatement = true;
20746-
break;
20747-
}
20748-
current = current.parent;
20749-
}
2075020744

20751-
if (containedInIterationStatement) {
20745+
const enclosingIterationStatement = getEnclosingIterationStatement(container);
20746+
if (enclosingIterationStatement) {
2075220747
if (usedInFunction) {
2075320748
// mark iteration statement as containing block-scoped binding captured in some function
2075420749
let capturesBlockScopeBindingInLoopBody = true;
@@ -20770,7 +20765,7 @@ namespace ts {
2077020765
}
2077120766
}
2077220767
if (capturesBlockScopeBindingInLoopBody) {
20773-
getNodeLinks(current).flags |= NodeCheckFlags.LoopWithCapturedBlockScopedBinding;
20768+
getNodeLinks(enclosingIterationStatement).flags |= NodeCheckFlags.LoopWithCapturedBlockScopedBinding;
2077420769
}
2077520770
}
2077620771

@@ -22369,6 +22364,20 @@ namespace ts {
2236922364
const links = getNodeLinks(node.expression);
2237022365
if (!links.resolvedType) {
2237122366
links.resolvedType = checkExpression(node.expression);
22367+
// The computed property name of a non-static class field within a loop must be stored in a block-scoped binding.
22368+
// (It needs to be bound at class evaluation time.)
22369+
if (isPropertyDeclaration(node.parent) && !hasStaticModifier(node.parent) && isClassExpression(node.parent.parent)) {
22370+
const container = getEnclosingBlockScopeContainer(node.parent.parent);
22371+
const enclosingIterationStatement = getEnclosingIterationStatement(container);
22372+
if (enclosingIterationStatement) {
22373+
// The computed field name will use a block scoped binding which can be unique for each iteration of the loop.
22374+
getNodeLinks(enclosingIterationStatement).flags |= NodeCheckFlags.LoopWithCapturedBlockScopedBinding;
22375+
// The generated variable which stores the computed field name must be block-scoped.
22376+
getNodeLinks(node).flags |= NodeCheckFlags.BlockScopedBindingInLoop;
22377+
// The generated variable which stores the class must be block-scoped.
22378+
getNodeLinks(node.parent.parent).flags |= NodeCheckFlags.BlockScopedBindingInLoop;
22379+
}
22380+
}
2237222381
// This will allow types number, string, symbol or any. It will also allow enums, the unknown
2237322382
// type, and any union of these types (like string | number).
2237422383
if (links.resolvedType.flags & TypeFlags.Nullable ||
@@ -28916,6 +28925,16 @@ namespace ts {
2891628925
for (let lexicalScope = getEnclosingBlockScopeContainer(node); !!lexicalScope; lexicalScope = getEnclosingBlockScopeContainer(lexicalScope)) {
2891728926
getNodeLinks(lexicalScope).flags |= NodeCheckFlags.ContainsClassWithPrivateIdentifiers;
2891828927
}
28928+
28929+
// If this is a private field in a class expression inside the body of a loop,
28930+
// then we must use a block-scoped binding to store the WeakMap.
28931+
if (isClassExpression(node.parent)) {
28932+
const enclosingIterationStatement = getEnclosingIterationStatement(node.parent);
28933+
if (enclosingIterationStatement) {
28934+
getNodeLinks(node.name).flags |= NodeCheckFlags.BlockScopedBindingInLoop;
28935+
getNodeLinks(enclosingIterationStatement).flags |= NodeCheckFlags.LoopWithCapturedBlockScopedBinding;
28936+
}
28937+
}
2891928938
}
2892028939
}
2892128940

src/compiler/factory.ts

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ namespace ts {
1818
resumeLexicalEnvironment: noop,
1919
startLexicalEnvironment: noop,
2020
suspendLexicalEnvironment: noop,
21+
startBlockScope: noop,
22+
endBlockScope: returnUndefined,
23+
addBlockScopedVariable: noop,
2124
addDiagnostic: noop,
2225
};
2326

src/compiler/transformer.ts

+46
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ namespace ts {
152152
let lexicalEnvironmentFunctionDeclarationsStack: FunctionDeclaration[][] = [];
153153
let lexicalEnvironmentStackOffset = 0;
154154
let lexicalEnvironmentSuspended = false;
155+
let blockScopedVariableDeclarationsStack: Identifier[][] = [];
156+
let blockScopeStackOffset = 0;
157+
let blockScopedVariableDeclarations: Identifier[];
155158
let emitHelpers: EmitHelper[] | undefined;
156159
let onSubstituteNode: TransformationContext["onSubstituteNode"] = noEmitSubstitution;
157160
let onEmitNode: TransformationContext["onEmitNode"] = noEmitNotification;
@@ -170,6 +173,9 @@ namespace ts {
170173
endLexicalEnvironment,
171174
hoistVariableDeclaration,
172175
hoistFunctionDeclaration,
176+
startBlockScope,
177+
endBlockScope,
178+
addBlockScopedVariable,
173179
requestEmitHelper,
174180
readEmitHelpers,
175181
enableSubstitution,
@@ -408,6 +414,46 @@ namespace ts {
408414
return statements;
409415
}
410416

417+
/**
418+
* Starts a block scope. Any existing block hoisted variables are pushed onto the stack and the related storage variables are reset.
419+
*/
420+
function startBlockScope() {
421+
Debug.assert(state > TransformationState.Uninitialized, "Cannot start a block scope during initialization.");
422+
Debug.assert(state < TransformationState.Completed, "Cannot start a block scope after transformation has completed.");
423+
blockScopedVariableDeclarationsStack[blockScopeStackOffset] = blockScopedVariableDeclarations;
424+
blockScopeStackOffset++;
425+
blockScopedVariableDeclarations = undefined!;
426+
}
427+
428+
/**
429+
* Ends a block scope. The previous set of block hoisted variables are restored. Any hoisted declarations are returned.
430+
*/
431+
function endBlockScope() {
432+
Debug.assert(state > TransformationState.Uninitialized, "Cannot end a block scope during initialization.");
433+
Debug.assert(state < TransformationState.Completed, "Cannot end a block scope after transformation has completed.");
434+
const statements: Statement[] | undefined = some(blockScopedVariableDeclarations) ?
435+
[
436+
createVariableStatement(
437+
/*modifiers*/ undefined,
438+
createVariableDeclarationList(
439+
blockScopedVariableDeclarations.map(identifier => createVariableDeclaration(identifier)),
440+
NodeFlags.Let
441+
)
442+
)
443+
] : undefined;
444+
blockScopeStackOffset--;
445+
blockScopedVariableDeclarations = blockScopedVariableDeclarationsStack[blockScopeStackOffset];
446+
if (blockScopeStackOffset === 0) {
447+
blockScopedVariableDeclarationsStack = [];
448+
}
449+
return statements;
450+
}
451+
452+
function addBlockScopedVariable(name: Identifier): void {
453+
Debug.assert(blockScopeStackOffset > 0, "Cannot add a block scoped variable outside of an iteration body.");
454+
(blockScopedVariableDeclarations || (blockScopedVariableDeclarations = [])).push(name);
455+
}
456+
411457
function requestEmitHelper(helper: EmitHelper): void {
412458
Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the transformation context during initialization.");
413459
Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed.");

src/compiler/transformers/classFields.ts

+25-10
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ namespace ts {
3535
const {
3636
hoistVariableDeclaration,
3737
endLexicalEnvironment,
38-
resumeLexicalEnvironment
38+
resumeLexicalEnvironment,
39+
addBlockScopedVariable
3940
} = context;
4041
const resolver = context.getEmitResolver();
4142
const compilerOptions = context.getCompilerOptions();
@@ -210,9 +211,12 @@ namespace ts {
210211
// Create a temporary variable to store a computed property name (if necessary).
211212
// If it's not inlineable, then we emit an expression after the class which assigns
212213
// the property name to the temporary variable.
213-
const expr = getPropertyNameExpressionIfNeeded(node.name, !!node.initializer || !!context.getCompilerOptions().useDefineForClassFields);
214-
if (expr && !isSimpleInlineableExpression(expr)) {
215-
(pendingExpressions || (pendingExpressions = [])).push(expr);
214+
// No need to transform static computed property names since they are only evaluated once.
215+
if (!hasStaticModifier(node)) {
216+
const expr = getPropertyNameExpressionIfNeeded(node.name, !!node.initializer || !!context.getCompilerOptions().useDefineForClassFields);
217+
if (expr && !isSimpleInlineableExpression(expr)) {
218+
(pendingExpressions || (pendingExpressions = [])).push(expr);
219+
}
216220
}
217221
return undefined;
218222
}
@@ -532,8 +536,10 @@ namespace ts {
532536
}
533537
else {
534538
const expressions: Expression[] = [];
535-
const isClassWithConstructorReference = resolver.getNodeCheckFlags(node) & NodeCheckFlags.ClassWithConstructorReference;
536-
const temp = createTempVariable(hoistVariableDeclaration, !!isClassWithConstructorReference);
539+
const classCheckFlags = resolver.getNodeCheckFlags(node);
540+
const isClassWithConstructorReference = classCheckFlags & NodeCheckFlags.ClassWithConstructorReference;
541+
const requiresBlockScopedVar = classCheckFlags & NodeCheckFlags.BlockScopedBindingInLoop;
542+
const temp = createTempVariable(requiresBlockScopedVar ? addBlockScopedVariable : hoistVariableDeclaration, !!isClassWithConstructorReference);
537543
if (isClassWithConstructorReference) {
538544
// record an alias as the class name is not in scope for statics.
539545
enableSubstitutionForClassAliases();
@@ -748,7 +754,7 @@ namespace ts {
748754
function transformProperty(property: PropertyDeclaration, receiver: LeftHandSideExpression) {
749755
// We generate a name here in order to reuse the value cached by the relocated computed name expression (which uses the same generated name)
750756
const emitAssignment = !context.getCompilerOptions().useDefineForClassFields;
751-
const propertyName = isComputedPropertyName(property.name) && !isSimpleInlineableExpression(property.name.expression)
757+
const propertyName = !hasStaticModifier(property) && isComputedPropertyName(property.name) && !isSimpleInlineableExpression(property.name.expression)
752758
? updateComputedPropertyName(property.name, getGeneratedNameForNode(property.name))
753759
: property.name;
754760

@@ -858,7 +864,6 @@ namespace ts {
858864
return undefined;
859865
}
860866

861-
862867
/**
863868
* If the name is a computed property, this function transforms it, then either returns an expression which caches the
864869
* value of the result or the expression itself if the value is either unused or safe to inline into multiple locations
@@ -872,7 +877,12 @@ namespace ts {
872877
const alreadyTransformed = isAssignmentExpression(innerExpression) && isGeneratedIdentifier(innerExpression.left);
873878
if (!alreadyTransformed && !inlinable && shouldHoist) {
874879
const generatedName = getGeneratedNameForNode(name);
875-
hoistVariableDeclaration(generatedName);
880+
if (resolver.getNodeCheckFlags(name) & NodeCheckFlags.BlockScopedBindingInLoop) {
881+
addBlockScopedVariable(generatedName);
882+
}
883+
else {
884+
hoistVariableDeclaration(generatedName);
885+
}
876886
return createAssignment(generatedName, expression);
877887
}
878888
return (inlinable || isIdentifier(innerExpression)) ? undefined : expression;
@@ -892,7 +902,12 @@ namespace ts {
892902
const text = getTextOfPropertyName(name) as string;
893903
const weakMapName = createOptimisticUniqueName("_" + text.substring(1));
894904
weakMapName.autoGenerateFlags |= GeneratedIdentifierFlags.ReservedInNestedScopes;
895-
hoistVariableDeclaration(weakMapName);
905+
if (resolver.getNodeCheckFlags(name) & NodeCheckFlags.BlockScopedBindingInLoop) {
906+
addBlockScopedVariable(weakMapName);
907+
}
908+
else {
909+
hoistVariableDeclaration(weakMapName);
910+
}
896911
(currentPrivateIdentifierEnvironment || (currentPrivateIdentifierEnvironment = createUnderscoreEscapedMap()))
897912
.set(name.escapedText, { placement: PrivateIdentifierPlacement.InstanceField, weakMapName });
898913
(pendingExpressions || (pendingExpressions = [])).push(

src/compiler/types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -5929,6 +5929,12 @@ namespace ts {
59295929
/** Hoists a variable declaration to the containing scope. */
59305930
hoistVariableDeclaration(node: Identifier): void;
59315931

5932+
/*@internal*/ startBlockScope(): void;
5933+
5934+
/*@internal*/ endBlockScope(): Statement[] | undefined;
5935+
5936+
/*@internal*/ addBlockScopedVariable(node: Identifier): void;
5937+
59325938
/** Records a request for a non-scoped emit helper in the current context. */
59335939
requestEmitHelper(helper: EmitHelper): void;
59345940

src/compiler/visitor.ts

+19
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,25 @@ namespace ts {
549549
return <Statement>singleOrUndefined(nodes) || createBlock(<NodeArray<Statement>>nodes);
550550
}
551551

552+
export function visitIterationBody(body: Statement, visitor: Visitor, context: TransformationContext): Statement {
553+
context.startBlockScope();
554+
const updated = visitNode(body, visitor, isStatement, liftToBlock);
555+
const declarations = context.endBlockScope();
556+
if (some(declarations)) {
557+
if (isBlock(updated)) {
558+
declarations.push(...updated.statements);
559+
}
560+
else {
561+
declarations.push(updated);
562+
}
563+
return setTextRange(
564+
setOriginalNode(createBlock(declarations), body),
565+
body
566+
);
567+
}
568+
return updated;
569+
}
570+
552571
/**
553572
* Aggregates the TransformFlags for a Node and its subtree.
554573
*/

src/compiler/visitorPublic.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -636,33 +636,33 @@ namespace ts {
636636

637637
case SyntaxKind.DoStatement:
638638
return updateDo(<DoStatement>node,
639-
visitNode((<DoStatement>node).statement, visitor, isStatement, liftToBlock),
639+
visitIterationBody((<DoStatement>node).statement, visitor, context),
640640
visitNode((<DoStatement>node).expression, visitor, isExpression));
641641

642642
case SyntaxKind.WhileStatement:
643643
return updateWhile(<WhileStatement>node,
644644
visitNode((<WhileStatement>node).expression, visitor, isExpression),
645-
visitNode((<WhileStatement>node).statement, visitor, isStatement, liftToBlock));
645+
visitIterationBody((<WhileStatement>node).statement, visitor, context));
646646

647647
case SyntaxKind.ForStatement:
648648
return updateFor(<ForStatement>node,
649649
visitNode((<ForStatement>node).initializer, visitor, isForInitializer),
650650
visitNode((<ForStatement>node).condition, visitor, isExpression),
651651
visitNode((<ForStatement>node).incrementor, visitor, isExpression),
652-
visitNode((<ForStatement>node).statement, visitor, isStatement, liftToBlock));
652+
visitIterationBody((<ForStatement>node).statement, visitor, context));
653653

654654
case SyntaxKind.ForInStatement:
655655
return updateForIn(<ForInStatement>node,
656656
visitNode((<ForInStatement>node).initializer, visitor, isForInitializer),
657657
visitNode((<ForInStatement>node).expression, visitor, isExpression),
658-
visitNode((<ForInStatement>node).statement, visitor, isStatement, liftToBlock));
658+
visitIterationBody((<ForInStatement>node).statement, visitor, context));
659659

660660
case SyntaxKind.ForOfStatement:
661661
return updateForOf(<ForOfStatement>node,
662662
visitNode((<ForOfStatement>node).awaitModifier, visitor, isToken),
663663
visitNode((<ForOfStatement>node).initializer, visitor, isForInitializer),
664664
visitNode((<ForOfStatement>node).expression, visitor, isExpression),
665-
visitNode((<ForOfStatement>node).statement, visitor, isStatement, liftToBlock));
665+
visitIterationBody((<ForOfStatement>node).statement, visitor, context));
666666

667667
case SyntaxKind.ContinueStatement:
668668
return updateContinue(<ContinueStatement>node,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//// [computedPropertyNames52.js]
2+
const array = [];
3+
for (let i = 0; i < 10; ++i) {
4+
array.push(class C {
5+
[i] = () => C;
6+
static [i] = 100;
7+
})
8+
}
9+
10+
11+
//// [computedPropertyNames52-emit.js]
12+
const array = [];
13+
for (let i = 0; i < 10; ++i) {
14+
let _a, _b;
15+
array.push((_b = class C {
16+
constructor() {
17+
this[_a] = () => C;
18+
}
19+
},
20+
_a = i,
21+
_b[i] = 100,
22+
_b));
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
=== tests/cases/conformance/es6/computedProperties/computedPropertyNames52.js ===
2+
const array = [];
3+
>array : Symbol(array, Decl(computedPropertyNames52.js, 0, 5))
4+
5+
for (let i = 0; i < 10; ++i) {
6+
>i : Symbol(i, Decl(computedPropertyNames52.js, 1, 8))
7+
>i : Symbol(i, Decl(computedPropertyNames52.js, 1, 8))
8+
>i : Symbol(i, Decl(computedPropertyNames52.js, 1, 8))
9+
10+
array.push(class C {
11+
>array.push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --))
12+
>array : Symbol(array, Decl(computedPropertyNames52.js, 0, 5))
13+
>push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --))
14+
>C : Symbol(C, Decl(computedPropertyNames52.js, 2, 15))
15+
16+
[i] = () => C;
17+
>[i] : Symbol(C[i], Decl(computedPropertyNames52.js, 2, 24))
18+
>i : Symbol(i, Decl(computedPropertyNames52.js, 1, 8))
19+
>C : Symbol(C, Decl(computedPropertyNames52.js, 2, 15))
20+
21+
static [i] = 100;
22+
>[i] : Symbol(C[i], Decl(computedPropertyNames52.js, 3, 22))
23+
>i : Symbol(i, Decl(computedPropertyNames52.js, 1, 8))
24+
25+
})
26+
}
27+

0 commit comments

Comments
 (0)