Skip to content

Commit 64a3d96

Browse files
author
Joseph Watts
committed
Fix computed property name bindings
Signed-off-by: Joseph Watts <[email protected]>
1 parent 6607e00 commit 64a3d96

17 files changed

+391
-16
lines changed

src/compiler/checker.ts

+26-13
Original file line numberDiff line numberDiff line change
@@ -16726,6 +16726,11 @@ namespace ts {
1672616726
if (container.kind === SyntaxKind.PropertyDeclaration && hasModifier(container, ModifierFlags.Static)) {
1672716727
getNodeLinks(declaration).flags |= NodeCheckFlags.ClassWithConstructorReference;
1672816728
getNodeLinks(node).flags |= NodeCheckFlags.ConstructorReferenceInClass;
16729+
// If the class expression is in a loop and the name of the class is used,
16730+
// the temporary variable which stores the evaluated class expression must be block scoped.
16731+
if (getEnclosingIterationStatement(declaration)) {
16732+
getNodeLinks(declaration).flags |= NodeCheckFlags.BlockScopedBindingInLoop;
16733+
}
1672916734
}
1673016735
break;
1673116736
}
@@ -16831,6 +16836,10 @@ namespace ts {
1683116836
return !!findAncestor(node, n => n === threshold ? "quit" : isFunctionLike(n));
1683216837
}
1683316838

16839+
function getEnclosingIterationStatement(node: Node): Node | undefined {
16840+
return findAncestor(node, n => (!n || nodeStartsNewLexicalEnvironment(n)) ? "quit" : isIterationStatement(n, /* lookInLabeledStatements */ false));
16841+
}
16842+
1683416843
function getPartOfForStatementContainingNode(node: Node, container: ForStatement) {
1683516844
return findAncestor(node, n => n === container ? "quit" : n === container.initializer || n === container.condition || n === container.incrementor || n === container.statement);
1683616845
}
@@ -16849,18 +16858,8 @@ namespace ts {
1684916858

1685016859
const container = getEnclosingBlockScopeContainer(symbol.valueDeclaration);
1685116860
const usedInFunction = isInsideFunction(node.parent, container);
16852-
let current = container;
16853-
16854-
let containedInIterationStatement = false;
16855-
while (current && !nodeStartsNewLexicalEnvironment(current)) {
16856-
if (isIterationStatement(current, /*lookInLabeledStatements*/ false)) {
16857-
containedInIterationStatement = true;
16858-
break;
16859-
}
16860-
current = current.parent;
16861-
}
16862-
16863-
if (containedInIterationStatement) {
16861+
const enclosingIterationStatement = getEnclosingIterationStatement(container);
16862+
if (enclosingIterationStatement) {
1686416863
if (usedInFunction) {
1686516864
// mark iteration statement as containing block-scoped binding captured in some function
1686616865
let capturesBlockScopeBindingInLoopBody = true;
@@ -16880,7 +16879,7 @@ namespace ts {
1688016879
}
1688116880
}
1688216881
if (capturesBlockScopeBindingInLoopBody) {
16883-
getNodeLinks(current).flags |= NodeCheckFlags.LoopWithCapturedBlockScopedBinding;
16882+
getNodeLinks(enclosingIterationStatement).flags |= NodeCheckFlags.LoopWithCapturedBlockScopedBinding;
1688416883
}
1688516884
}
1688616885

@@ -18439,6 +18438,20 @@ namespace ts {
1843918438
const links = getNodeLinks(node.expression);
1844018439
if (!links.resolvedType) {
1844118440
links.resolvedType = checkExpression(node.expression);
18441+
18442+
if (isPropertyDeclaration(node.parent) && isClassExpression(node.parent.parent)) {
18443+
const container = getEnclosingBlockScopeContainer(node);
18444+
const enclosingIterationStatement = getEnclosingIterationStatement(container);
18445+
// A computed property of a class expression inside a loop must be block scoped because
18446+
// the property name should be bound at class evaluation time.
18447+
if (enclosingIterationStatement) {
18448+
getNodeLinks(enclosingIterationStatement).flags |= NodeCheckFlags.LoopWithCapturedBlockScopedBinding;
18449+
// The hoisted variable which stores the evaluated property name should be block scoped.
18450+
getNodeLinks(node).flags |= NodeCheckFlags.BlockScopedBindingInLoop;
18451+
// The temporary name of the class expression should be block scoped.
18452+
getNodeLinks(node.parent.parent).flags |= NodeCheckFlags.BlockScopedBindingInLoop;
18453+
}
18454+
}
1844218455
// This will allow types number, string, symbol or any. It will also allow enums, the unknown
1844318456
// type, and any union of these types (like string | number).
1844418457
if (links.resolvedType.flags & TypeFlags.Nullable ||

src/compiler/factory.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3172,6 +3172,8 @@ namespace ts {
31723172
enableEmitNotification: noop,
31733173
enableSubstitution: noop,
31743174
endLexicalEnvironment: () => undefined,
3175+
endBlockScope: () => undefined,
3176+
startBlockScope: noop,
31753177
getCompilerOptions: notImplemented,
31763178
getEmitHost: notImplemented,
31773179
getEmitResolver: notImplemented,

src/compiler/transformer.ts

+45
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ namespace ts {
102102
let lexicalEnvironmentFunctionDeclarationsStack: FunctionDeclaration[][] = [];
103103
let lexicalEnvironmentStackOffset = 0;
104104
let lexicalEnvironmentSuspended = false;
105+
let blockScopedVariableDeclarationsStack: Identifier[][] = [];
106+
let blockScopeStackOffset = 0;
107+
let blockScopedVariableDeclarations: Identifier[];
105108
let emitHelpers: EmitHelper[] | undefined;
106109
let onSubstituteNode: TransformationContext["onSubstituteNode"] = noEmitSubstitution;
107110
let onEmitNode: TransformationContext["onEmitNode"] = noEmitNotification;
@@ -120,6 +123,8 @@ namespace ts {
120123
endLexicalEnvironment,
121124
hoistVariableDeclaration,
122125
hoistFunctionDeclaration,
126+
startBlockScope,
127+
endBlockScope,
123128
requestEmitHelper,
124129
readEmitHelpers,
125130
enableSubstitution,
@@ -247,6 +252,11 @@ namespace ts {
247252
function hoistVariableDeclaration(name: Identifier): void {
248253
Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization.");
249254
Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed.");
255+
// If the checker determined that this is a block scoped binding in a loop, we must emit a block-level variable declaration.
256+
if (resolver && resolver.getNodeCheckFlags(name) & NodeCheckFlags.BlockScopedBindingInLoop) {
257+
(blockScopedVariableDeclarations || (blockScopedVariableDeclarations = [])).push(name);
258+
return;
259+
}
250260
const decl = setEmitFlags(createVariableDeclaration(name), EmitFlags.NoNestedSourceMaps);
251261
if (!lexicalEnvironmentVariableDeclarations) {
252262
lexicalEnvironmentVariableDeclarations = [decl];
@@ -270,6 +280,41 @@ namespace ts {
270280
}
271281
}
272282

283+
/**
284+
* Starts a block scope. Any existing block hoisted variables are pushed onto the stack and the related storage variables are reset.
285+
*/
286+
function startBlockScope() {
287+
Debug.assert(state > TransformationState.Uninitialized, "Cannot start a block scope during initialization.");
288+
Debug.assert(state < TransformationState.Completed, "Cannot start a block scope after transformation has completed.");
289+
blockScopedVariableDeclarationsStack[blockScopeStackOffset] = blockScopedVariableDeclarations;
290+
blockScopeStackOffset++;
291+
blockScopedVariableDeclarations = undefined!;
292+
}
293+
294+
/**
295+
* Ends a block scope. The previous set of block hoisted variables are restored. Any hoisted declarations are returned.
296+
*/
297+
function endBlockScope() {
298+
Debug.assert(state > TransformationState.Uninitialized, "Cannot end a block scope during initialization.");
299+
Debug.assert(state < TransformationState.Completed, "Cannot end a block scope after transformation has completed.");
300+
const statements: Statement[] | undefined = some(blockScopedVariableDeclarations) ?
301+
[
302+
createVariableStatement(
303+
/*modifiers*/ undefined,
304+
createVariableDeclarationList(
305+
blockScopedVariableDeclarations.map(identifier => createVariableDeclaration(identifier)),
306+
NodeFlags.Let
307+
)
308+
)
309+
] : undefined;
310+
blockScopeStackOffset--;
311+
blockScopedVariableDeclarations = blockScopedVariableDeclarationsStack[blockScopeStackOffset];
312+
if (blockScopeStackOffset === 0) {
313+
blockScopedVariableDeclarationsStack = [];
314+
}
315+
return statements;
316+
}
317+
273318
/**
274319
* Starts a new lexical environment. Any existing hoisted variable or function declarations
275320
* are pushed onto a stack, and the related storage variables are reset.

src/compiler/transformers/ts.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ namespace ts {
3737
resumeLexicalEnvironment,
3838
endLexicalEnvironment,
3939
hoistVariableDeclaration,
40+
startBlockScope,
41+
endBlockScope
4042
} = context;
4143

4244
const resolver = context.getEmitResolver();
@@ -360,6 +362,8 @@ namespace ts {
360362
}
361363

362364
switch (node.kind) {
365+
case SyntaxKind.Block:
366+
return visitBlock(node as Block);
363367
case SyntaxKind.ExportKeyword:
364368
case SyntaxKind.DefaultKeyword:
365369
// ES6 export and default modifiers are elided when inside a namespace.
@@ -549,6 +553,19 @@ namespace ts {
549553
}
550554
}
551555

556+
function visitBlock(node: Block): Block {
557+
startBlockScope();
558+
node = visitEachChild(node, visitor, context);
559+
const declarations = endBlockScope();
560+
if (some(declarations)) {
561+
return updateBlock(
562+
node,
563+
mergeLexicalEnvironment(node.statements, declarations)
564+
);
565+
}
566+
return node;
567+
}
568+
552569
function visitSourceFile(node: SourceFile) {
553570
const alwaysStrict = getStrictOptionValue(compilerOptions, "alwaysStrict") &&
554571
!(isExternalModule(node) && moduleKind >= ModuleKind.ES2015) &&
@@ -909,7 +926,13 @@ namespace ts {
909926
if (some(staticProperties) || some(pendingExpressions)) {
910927
const expressions: Expression[] = [];
911928
const isClassWithConstructorReference = resolver.getNodeCheckFlags(node) & NodeCheckFlags.ClassWithConstructorReference;
912-
const temp = createTempVariable(hoistVariableDeclaration, !!isClassWithConstructorReference);
929+
const temp = createTempVariable(
930+
name => {
931+
setOriginalNode(name, node);
932+
hoistVariableDeclaration(name);
933+
},
934+
!!isClassWithConstructorReference
935+
);
913936
if (isClassWithConstructorReference) {
914937
// record an alias as the class name is not in scope for statics.
915938
enableSubstitutionForClassAliases();
@@ -2185,6 +2208,7 @@ namespace ts {
21852208
const inlinable = isSimpleInlineableExpression(innerExpression);
21862209
if (!inlinable && shouldHoist) {
21872210
const generatedName = getGeneratedNameForNode(name);
2211+
setOriginalNode(generatedName, name);
21882212
hoistVariableDeclaration(generatedName);
21892213
return createAssignment(generatedName, expression);
21902214
}

src/compiler/types.ts

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

5308+
/* @internal */
5309+
startBlockScope(): void;
5310+
5311+
/* @internal */
5312+
endBlockScope(): Statement[] | undefined;
5313+
53085314
/** Records a request for a non-scoped emit helper in the current context. */
53095315
requestEmitHelper(helper: EmitHelper): void;
53105316

tests/baselines/reference/classExpressionWithStaticProperties3.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ for (let i = 0; i < 3; i++) {
1010
arr.forEach(C => console.log(C.y()));
1111

1212
//// [classExpressionWithStaticProperties3.js]
13-
var _a;
1413
var arr = [];
1514
var _loop_1 = function (i) {
15+
var _a = void 0;
1616
arr.push((_a = /** @class */ (function () {
1717
function C() {
1818
}

tests/baselines/reference/classExpressionWithStaticPropertiesES63.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ for (let i = 0; i < 3; i++) {
1010
arr.forEach(C => console.log(C.y()));
1111

1212
//// [classExpressionWithStaticPropertiesES63.js]
13-
var _a;
1413
const arr = [];
1514
for (let i = 0; i < 3; i++) {
15+
let _a;
1616
arr.push((_a = class C {
1717
},
1818
_a.x = i,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
tests/cases/conformance/es6/computedProperties/computedPropertyNames52_ES5.ts(5,13): error TS1166: A computed property name in a class property declaration must refer to an expression whose type is a literal type or a 'unique symbol' type.
2+
3+
4+
==== tests/cases/conformance/es6/computedProperties/computedPropertyNames52_ES5.ts (1 errors) ====
5+
const classes = [];
6+
for (let i = 0; i <= 10; ++i) {
7+
classes.push(
8+
class A {
9+
[i] = "my property";
10+
~~~
11+
!!! error TS1166: A computed property name in a class property declaration must refer to an expression whose type is a literal type or a 'unique symbol' type.
12+
}
13+
);
14+
}
15+
for (const clazz of classes) {
16+
console.log(Object.getOwnPropertyNames(new clazz()));
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//// [computedPropertyNames52_ES5.ts]
2+
const classes = [];
3+
for (let i = 0; i <= 10; ++i) {
4+
classes.push(
5+
class A {
6+
[i] = "my property";
7+
}
8+
);
9+
}
10+
for (const clazz of classes) {
11+
console.log(Object.getOwnPropertyNames(new clazz()));
12+
}
13+
14+
//// [computedPropertyNames52_ES5.js]
15+
var classes = [];
16+
var _loop_1 = function (i) {
17+
var _a = void 0, _b = void 0;
18+
classes.push((_b = /** @class */ (function () {
19+
function A() {
20+
this[_a] = "my property";
21+
}
22+
return A;
23+
}()),
24+
_a = i,
25+
_b));
26+
};
27+
for (var i = 0; i <= 10; ++i) {
28+
_loop_1(i);
29+
}
30+
for (var _i = 0, classes_1 = classes; _i < classes_1.length; _i++) {
31+
var clazz = classes_1[_i];
32+
console.log(Object.getOwnPropertyNames(new clazz()));
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
=== tests/cases/conformance/es6/computedProperties/computedPropertyNames52_ES5.ts ===
2+
const classes = [];
3+
>classes : Symbol(classes, Decl(computedPropertyNames52_ES5.ts, 0, 5))
4+
5+
for (let i = 0; i <= 10; ++i) {
6+
>i : Symbol(i, Decl(computedPropertyNames52_ES5.ts, 1, 8))
7+
>i : Symbol(i, Decl(computedPropertyNames52_ES5.ts, 1, 8))
8+
>i : Symbol(i, Decl(computedPropertyNames52_ES5.ts, 1, 8))
9+
10+
classes.push(
11+
>classes.push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --))
12+
>classes : Symbol(classes, Decl(computedPropertyNames52_ES5.ts, 0, 5))
13+
>push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --))
14+
15+
class A {
16+
>A : Symbol(A, Decl(computedPropertyNames52_ES5.ts, 2, 17))
17+
18+
[i] = "my property";
19+
>[i] : Symbol(A[i], Decl(computedPropertyNames52_ES5.ts, 3, 17))
20+
>i : Symbol(i, Decl(computedPropertyNames52_ES5.ts, 1, 8))
21+
}
22+
);
23+
}
24+
for (const clazz of classes) {
25+
>clazz : Symbol(clazz, Decl(computedPropertyNames52_ES5.ts, 8, 10))
26+
>classes : Symbol(classes, Decl(computedPropertyNames52_ES5.ts, 0, 5))
27+
28+
console.log(Object.getOwnPropertyNames(new clazz()));
29+
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
30+
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
31+
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
32+
>Object.getOwnPropertyNames : Symbol(ObjectConstructor.getOwnPropertyNames, Decl(lib.es5.d.ts, --, --))
33+
>Object : Symbol(Object, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
34+
>getOwnPropertyNames : Symbol(ObjectConstructor.getOwnPropertyNames, Decl(lib.es5.d.ts, --, --))
35+
>clazz : Symbol(clazz, Decl(computedPropertyNames52_ES5.ts, 8, 10))
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
=== tests/cases/conformance/es6/computedProperties/computedPropertyNames52_ES5.ts ===
2+
const classes = [];
3+
>classes : any[]
4+
>[] : undefined[]
5+
6+
for (let i = 0; i <= 10; ++i) {
7+
>i : number
8+
>0 : 0
9+
>i <= 10 : boolean
10+
>i : number
11+
>10 : 10
12+
>++i : number
13+
>i : number
14+
15+
classes.push(
16+
>classes.push( class A { [i] = "my property"; } ) : number
17+
>classes.push : (...items: any[]) => number
18+
>classes : any[]
19+
>push : (...items: any[]) => number
20+
21+
class A {
22+
>class A { [i] = "my property"; } : typeof A
23+
>A : typeof A
24+
25+
[i] = "my property";
26+
>[i] : string
27+
>i : number
28+
>"my property" : "my property"
29+
}
30+
);
31+
}
32+
for (const clazz of classes) {
33+
>clazz : any
34+
>classes : any[]
35+
36+
console.log(Object.getOwnPropertyNames(new clazz()));
37+
>console.log(Object.getOwnPropertyNames(new clazz())) : void
38+
>console.log : (message?: any, ...optionalParams: any[]) => void
39+
>console : Console
40+
>log : (message?: any, ...optionalParams: any[]) => void
41+
>Object.getOwnPropertyNames(new clazz()) : string[]
42+
>Object.getOwnPropertyNames : (o: any) => string[]
43+
>Object : ObjectConstructor
44+
>getOwnPropertyNames : (o: any) => string[]
45+
>new clazz() : any
46+
>clazz : any
47+
}

0 commit comments

Comments
 (0)