Skip to content

Commit b7ea7f4

Browse files
author
Joseph Watts
committed
Downlevel private named field initializers
Signed-off-by: Joseph Watts <[email protected]>
1 parent 979155c commit b7ea7f4

18 files changed

+274
-20
lines changed

src/compiler/binder.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3637,6 +3637,10 @@ namespace ts {
36373637
let excludeFlags = TransformFlags.NodeExcludes;
36383638

36393639
switch (kind) {
3640+
case SyntaxKind.PrivateName:
3641+
// Private names are ESNext syntax.
3642+
transformFlags |= TransformFlags.AssertESNext;
3643+
break;
36403644
case SyntaxKind.AsyncKeyword:
36413645
case SyntaxKind.AwaitExpression:
36423646
// async/await is ES2017 syntax, but may be ESNext syntax (for async generators)

src/compiler/transformers/esnext.ts

Lines changed: 106 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,23 @@ namespace ts {
1010
ClassAliases = 1 << 1,
1111
}
1212

13+
/**
14+
* A mapping of private names to information needed for transformation.
15+
*/
16+
type PrivateNameEnvironment = UnderscoreEscapedMap<PrivateNamedInstanceField>;
17+
18+
/**
19+
* Identifies the type of private name.
20+
*/
21+
const enum PrivateNameType {
22+
InstanceField
23+
}
24+
25+
interface PrivateNamedInstanceField {
26+
type: PrivateNameType.InstanceField;
27+
weakMapName: Identifier;
28+
}
29+
1330
export function transformESNext(context: TransformationContext) {
1431
const {
1532
resumeLexicalEnvironment,
@@ -52,6 +69,8 @@ namespace ts {
5269
*/
5370
let pendingStatements: Statement[] | undefined;
5471

72+
const privateNameEnvironmentStack: PrivateNameEnvironment[] = [];
73+
5574
return chainBundle(transformSourceFile);
5675

5776
function transformSourceFile(node: SourceFile) {
@@ -230,6 +249,7 @@ namespace ts {
230249
function visitClassDeclaration(node: ClassDeclaration) {
231250
const savedPendingExpressions = pendingExpressions;
232251
pendingExpressions = undefined;
252+
startPrivateNameEnvironment();
233253

234254
const extendsClauseElement = getEffectiveBaseTypeNode(node);
235255
const isDerivedClass = !!(extendsClauseElement && skipOuterExpressions(extendsClauseElement.expression).kind !== SyntaxKind.NullKeyword);
@@ -250,6 +270,7 @@ namespace ts {
250270
if (some(pendingExpressions)) {
251271
statements.push(createExpressionStatement(inlineExpressions(pendingExpressions!)));
252272
}
273+
endPrivateNameEnvironment();
253274
pendingExpressions = savedPendingExpressions;
254275

255276
// Emit static property assignment. Because classDeclaration is lexically evaluated,
@@ -268,6 +289,7 @@ namespace ts {
268289
function visitClassExpression(node: ClassExpression): Expression {
269290
const savedPendingExpressions = pendingExpressions;
270291
pendingExpressions = undefined;
292+
startPrivateNameEnvironment();
271293

272294
// If this class expression is a transformation of a decorated class declaration,
273295
// then we want to output the pendingExpressions as statements, not as inlined
@@ -295,7 +317,7 @@ namespace ts {
295317
if (isDecoratedClassDeclaration) {
296318
Debug.assertDefined(pendingStatements, "Decorated classes transformed by TypeScript are expected to be within a variable declaration.");
297319

298-
// Write any pending expressions from elided or moved computed property names
320+
// Write any pending expressions from elided or moved computed property names or private names
299321
if (some(pendingExpressions)) {
300322
pendingStatements!.push(createExpressionStatement(inlineExpressions(pendingExpressions!)));
301323
}
@@ -304,6 +326,7 @@ namespace ts {
304326
if (some(staticProperties)) {
305327
addInitializedPropertyStatements(pendingStatements!, staticProperties, getInternalName(node));
306328
}
329+
endPrivateNameEnvironment();
307330
return classExpression;
308331
}
309332
else {
@@ -328,28 +351,38 @@ namespace ts {
328351
expressions.push(startOnNewLine(temp));
329352

330353
pendingExpressions = savedPendingExpressions;
354+
endPrivateNameEnvironment();
331355
return inlineExpressions(expressions);
332356
}
333357
}
334358

335359
pendingExpressions = savedPendingExpressions;
360+
endPrivateNameEnvironment();
336361
return classExpression;
337362
}
338363

339364
function transformClassMembers(node: ClassDeclaration | ClassExpression, isDerivedClass: boolean) {
365+
// Declare private names.
366+
const privateNamedProperties = filter(node.members, isPrivateNamedPropertyDeclaration);
367+
privateNamedProperties.forEach(property => addPrivateNameToEnvironment(property.name));
368+
340369
const members: ClassElement[] = [];
341370
const constructor = transformConstructor(node, isDerivedClass);
342371
if (constructor) {
343372
members.push(constructor);
344373
}
374+
345375
addRange(members, visitNodes(node.members, classElementVisitor, isClassElement));
346376
return setTextRange(createNodeArray(members), /*location*/ node.members);
347377
}
348378

349379
function transformConstructor(node: ClassDeclaration | ClassExpression, isDerivedClass: boolean) {
350380
const constructor = visitNode(getFirstConstructorWithBody(node), visitor, isConstructorDeclaration);
351-
const containsPropertyInitializer = forEach(node.members, isInitializedProperty);
352-
if (!containsPropertyInitializer) {
381+
const containsPropertyInitializerOrPrivateProperty = forEach(
382+
node.members,
383+
member => isInitializedProperty(member) || isPrivateNamedPropertyDeclaration(member)
384+
);
385+
if (!containsPropertyInitializerOrPrivateProperty) {
353386
return constructor;
354387
}
355388
const parameters = visitParameterList(constructor ? constructor.parameters : undefined, visitor, context);
@@ -374,9 +407,9 @@ namespace ts {
374407
}
375408

376409
function transformConstructorBody(node: ClassDeclaration | ClassExpression, constructor: ConstructorDeclaration | undefined, isDerivedClass: boolean) {
377-
const properties = getInitializedProperties(node, /*isStatic*/ false);
410+
const properties = filter(node.members, (node): node is PropertyDeclaration => isPropertyDeclaration(node) && !hasStaticModifier(node));
378411

379-
// Only generate synthetic constructor when there are property initializers to move.
412+
// Only generate synthetic constructor when there are property declarations to move.
380413
if (!constructor && !some(properties)) {
381414
return visitFunctionBody(/*node*/ undefined, visitor, context);
382415
}
@@ -445,7 +478,11 @@ namespace ts {
445478
*/
446479
function addInitializedPropertyStatements(statements: Statement[], properties: ReadonlyArray<PropertyDeclaration>, receiver: LeftHandSideExpression) {
447480
for (const property of properties) {
448-
const statement = createExpressionStatement(transformInitializedProperty(property, receiver));
481+
const expression = transformProperty(property, receiver);
482+
if (!expression) {
483+
continue;
484+
}
485+
const statement = createExpressionStatement(expression);
449486
setSourceMapRange(statement, moveRangePastModifiers(property));
450487
setCommentRange(statement, property);
451488
setOriginalNode(statement, property);
@@ -462,7 +499,10 @@ namespace ts {
462499
function generateInitializedPropertyExpressions(properties: ReadonlyArray<PropertyDeclaration>, receiver: LeftHandSideExpression) {
463500
const expressions: Expression[] = [];
464501
for (const property of properties) {
465-
const expression = transformInitializedProperty(property, receiver);
502+
const expression = transformProperty(property, receiver);
503+
if (!expression) {
504+
continue;
505+
}
466506
startOnNewLine(expression);
467507
setSourceMapRange(expression, moveRangePastModifiers(property));
468508
setCommentRange(expression, property);
@@ -479,17 +519,74 @@ namespace ts {
479519
* @param property The property declaration.
480520
* @param receiver The object receiving the property assignment.
481521
*/
482-
function transformInitializedProperty(property: PropertyDeclaration, receiver: LeftHandSideExpression) {
522+
function transformProperty(property: PropertyDeclaration, receiver: LeftHandSideExpression) {
483523
// We generate a name here in order to reuse the value cached by the relocated computed name expression (which uses the same generated name)
484524
const propertyName = isComputedPropertyName(property.name) && !isSimpleInlineableExpression(property.name.expression)
485525
? updateComputedPropertyName(property.name, getGeneratedNameForNode(property.name))
486526
: property.name;
487-
const initializer = visitNode(property.initializer!, visitor, isExpression);
527+
const initializer = visitNode(property.initializer, visitor, isExpression);
528+
529+
if (isPrivateName(propertyName)) {
530+
const privateNameInfo = accessPrivateName(propertyName);
531+
if (privateNameInfo) {
532+
switch (privateNameInfo.type) {
533+
case PrivateNameType.InstanceField: {
534+
return createCall(
535+
createPropertyAccess(privateNameInfo.weakMapName, "set"),
536+
/*typeArguments*/ undefined,
537+
[receiver, initializer || createVoidZero()]
538+
);
539+
}
540+
}
541+
}
542+
}
543+
if (!initializer) {
544+
return undefined;
545+
}
488546
const memberAccess = createMemberAccessForPropertyName(receiver, propertyName, /*location*/ propertyName);
489547

490548
return createAssignment(memberAccess, initializer);
491549
}
492550

551+
function startPrivateNameEnvironment() {
552+
const env: PrivateNameEnvironment = createUnderscoreEscapedMap();
553+
privateNameEnvironmentStack.push(env);
554+
return env;
555+
}
556+
557+
function endPrivateNameEnvironment() {
558+
privateNameEnvironmentStack.pop();
559+
}
560+
561+
function addPrivateNameToEnvironment(name: PrivateName) {
562+
const env = last(privateNameEnvironmentStack);
563+
const text = getTextOfPropertyName(name) as string;
564+
const weakMapName = createOptimisticUniqueName("_" + text.substring(1));
565+
weakMapName.autoGenerateFlags |= GeneratedIdentifierFlags.ReservedInNestedScopes;
566+
hoistVariableDeclaration(weakMapName);
567+
env.set(name.escapedText, { type: PrivateNameType.InstanceField, weakMapName });
568+
(pendingExpressions || (pendingExpressions = [])).push(
569+
createAssignment(
570+
weakMapName,
571+
createNew(
572+
createIdentifier("WeakMap"),
573+
/*typeArguments*/ undefined,
574+
[]
575+
)
576+
)
577+
);
578+
}
579+
580+
function accessPrivateName(name: PrivateName) {
581+
for (let i = privateNameEnvironmentStack.length - 1; i >= 0; --i) {
582+
const env = privateNameEnvironmentStack[i];
583+
if (env.has(name.escapedText)) {
584+
return env.get(name.escapedText);
585+
}
586+
}
587+
return undefined;
588+
}
589+
493590
function enableSubstitutionForClassAliases() {
494591
if ((enabledSubstitutions & ESNextSubstitutionFlags.ClassAliases) === 0) {
495592
enabledSubstitutions |= ESNextSubstitutionFlags.ClassAliases;

src/compiler/transformers/ts.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -929,17 +929,12 @@ namespace ts {
929929
const parameters = transformConstructorParameters(constructor);
930930
const body = transformConstructorBody(node.members, constructor, parametersWithPropertyAssignments);
931931
members.push(startOnNewLine(
932-
setOriginalNode(
933-
setTextRange(
934-
createConstructor(
935-
/*decorators*/ undefined,
936-
/*modifiers*/ undefined,
937-
parameters,
938-
body
939-
),
940-
constructor
941-
),
942-
constructor
932+
updateConstructor(
933+
constructor,
934+
/*decorators*/ undefined,
935+
/*modifiers*/ undefined,
936+
parameters,
937+
body
943938
)
944939
));
945940
addRange(

src/compiler/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,11 @@ namespace ts {
942942
initializer?: Expression; // Optional initializer
943943
}
944944

945+
/*@internal*/
946+
export interface PrivateNamedPropertyDeclaration extends PropertyDeclaration {
947+
name: PrivateName;
948+
}
949+
945950
export interface ObjectLiteralElement extends NamedDeclaration {
946951
_objectLiteralBrandBrand: any;
947952
name?: PropertyName;

src/compiler/utilities.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6111,6 +6111,11 @@ namespace ts {
61116111
|| kind === SyntaxKind.ComputedPropertyName;
61126112
}
61136113

6114+
/*@internal*/
6115+
export function isPrivateNamedPropertyDeclaration(node: Node): node is PrivateNamedPropertyDeclaration {
6116+
return isPropertyDeclaration(node) && isPrivateName(node.name);
6117+
}
6118+
61146119
export function isBindingName(node: Node): node is BindingName {
61156120
const kind = node.kind;
61166121
return kind === SyntaxKind.Identifier

tests/baselines/reference/privateNameField.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ class A {
77
}
88

99
//// [privateNameField.js]
10+
var _name;
1011
var A = /** @class */ (function () {
1112
function A(name) {
13+
_name.set(this, void 0);
1214
this.#name = name;
1315
}
1416
return A;
1517
}());
18+
_name = new WeakMap();
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//// [privateNameFieldDeclaration.ts]
2+
class A {
3+
#name: string;
4+
}
5+
6+
7+
//// [privateNameFieldDeclaration.js]
8+
var _name;
9+
var A = /** @class */ (function () {
10+
function A() {
11+
_name.set(this, void 0);
12+
}
13+
return A;
14+
}());
15+
_name = new WeakMap();
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== tests/cases/conformance/classes/members/privateNames/privateNameFieldDeclaration.ts ===
2+
class A {
3+
>A : Symbol(A, Decl(privateNameFieldDeclaration.ts, 0, 0))
4+
5+
#name: string;
6+
>#name : Symbol(A.#name, Decl(privateNameFieldDeclaration.ts, 0, 9))
7+
}
8+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== tests/cases/conformance/classes/members/privateNames/privateNameFieldDeclaration.ts ===
2+
class A {
3+
>A : A
4+
5+
#name: string;
6+
>#name : string
7+
}
8+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//// [privateNameFieldInitializer.ts]
2+
class A {
3+
#field = 10;
4+
#uninitialized;
5+
}
6+
7+
8+
//// [privateNameFieldInitializer.js]
9+
var _field, _uninitialized;
10+
var A = /** @class */ (function () {
11+
function A() {
12+
_field.set(this, 10);
13+
_uninitialized.set(this, void 0);
14+
}
15+
return A;
16+
}());
17+
_field = new WeakMap(), _uninitialized = new WeakMap();

0 commit comments

Comments
 (0)