Skip to content

Commit 229f3ae

Browse files
author
Joseph Watts
committed
Transform property access for private named instance fields
Signed-off-by: Joseph Watts <jwatts43@bloomberg.net>
1 parent b7ea7f4 commit 229f3ae

18 files changed

+911
-24
lines changed

src/compiler/transformers/esnext.ts

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ namespace ts {
149149
if (capturedSuperProperties && isPropertyAccessExpression(node) && node.expression.kind === SyntaxKind.SuperKeyword) {
150150
capturedSuperProperties.set(node.name.escapedText, true);
151151
}
152-
return visitEachChild(node, visitor, context);
152+
return visitPropertyAccessExpression(node as PropertyAccessExpression);
153153
case SyntaxKind.ElementAccessExpression:
154154
if (capturedSuperProperties && (<ElementAccessExpression>node).expression.kind === SyntaxKind.SuperKeyword) {
155155
hasSuperElementAccess = true;
@@ -165,6 +165,10 @@ namespace ts {
165165
return visitVariableStatement(node as VariableStatement);
166166
case SyntaxKind.ComputedPropertyName:
167167
return visitComputedPropertyName(node as ComputedPropertyName);
168+
case SyntaxKind.PrefixUnaryExpression:
169+
return visitPrefixUnaryExpression(node as PrefixUnaryExpression);
170+
case SyntaxKind.PostfixUnaryExpression:
171+
return visitPostfixUnaryExpression(node as PostfixUnaryExpression);
168172
default:
169173
return visitEachChild(node, visitor, context);
170174
}
@@ -587,6 +591,98 @@ namespace ts {
587591
return undefined;
588592
}
589593

594+
function visitPropertyAccessExpression(node: PropertyAccessExpression) {
595+
if (isPrivateName(node.name)) {
596+
const privateNameInfo = accessPrivateName(node.name);
597+
if (privateNameInfo) {
598+
switch (privateNameInfo.type) {
599+
case PrivateNameType.InstanceField:
600+
return setOriginalNode(
601+
setTextRange(
602+
createClassPrivateFieldGetHelper(
603+
context,
604+
visitNode(node.expression, visitor, isExpression),
605+
privateNameInfo.weakMapName
606+
),
607+
node
608+
),
609+
node
610+
);
611+
}
612+
}
613+
}
614+
return visitEachChild(node, visitor, context);
615+
}
616+
617+
function visitPrefixUnaryExpression(node: PrefixUnaryExpression) {
618+
if (isPrivateNamedPropertyAccessExpression(node.operand)) {
619+
const operator = (node.operator === SyntaxKind.PlusPlusToken) ?
620+
SyntaxKind.PlusEqualsToken : (node.operator === SyntaxKind.MinusMinusToken) ?
621+
SyntaxKind.MinusEqualsToken : undefined;
622+
if (operator) {
623+
const transformedExpr = setOriginalNode(
624+
createBinary(
625+
node.operand,
626+
operator,
627+
createNumericLiteral("1")
628+
),
629+
node
630+
);
631+
const visited = visitNode(transformedExpr, visitor);
632+
// If the private name was successfully transformed,
633+
// return the transformed node. Otherwise, leave existing source untouched.
634+
if (visited !== transformedExpr) {
635+
return visited;
636+
}
637+
}
638+
}
639+
return visitEachChild(node, visitor, context);
640+
}
641+
642+
function visitPostfixUnaryExpression(node: PostfixUnaryExpression) {
643+
if (isPrivateNamedPropertyAccessExpression(node.operand)) {
644+
const operator = (node.operator === SyntaxKind.PlusPlusToken) ?
645+
SyntaxKind.PlusToken : (node.operator === SyntaxKind.MinusMinusToken) ?
646+
SyntaxKind.MinusToken : undefined;
647+
if (operator) {
648+
// Create a temporary variable if the receiver is not inlinable, since we
649+
// will need to access it multiple times.
650+
const receiver = isSimpleInlineableExpression(node.operand.expression) ?
651+
undefined :
652+
getGeneratedNameForNode(node.operand.expression);
653+
// Create a temporary variable to store the value returned by the expression.
654+
const returnValue = createTempVariable(/*recordTempVariable*/ undefined);
655+
656+
const transformedExpr = createCommaList(compact([
657+
receiver && createAssignment(receiver, node.operand.expression),
658+
// Store the existing value of the private name in the temporary.
659+
createAssignment(returnValue, receiver ? createPropertyAccess(receiver, node.operand.name) : node.operand),
660+
// Assign to private name.
661+
createAssignment(
662+
receiver ? createPropertyAccess(receiver, node.operand.name) : node.operand,
663+
createBinary(
664+
returnValue, operator, createNumericLiteral("1")
665+
)
666+
),
667+
// Return the cached value.
668+
returnValue
669+
]) as Expression[]);
670+
const visited = visitNode(transformedExpr, visitor);
671+
// If the private name was successfully transformed,
672+
// hoist the temporary variable and return the transformed node.
673+
// Otherwise, leave existing source untouched.
674+
if (visited !== transformedExpr) {
675+
if (receiver) {
676+
hoistVariableDeclaration(receiver);
677+
}
678+
hoistVariableDeclaration(returnValue);
679+
return visited;
680+
}
681+
}
682+
}
683+
return visitEachChild(node, visitor, context);
684+
}
685+
590686
function enableSubstitutionForClassAliases() {
591687
if ((enabledSubstitutions & ESNextSubstitutionFlags.ClassAliases) === 0) {
592688
enabledSubstitutions |= ESNextSubstitutionFlags.ClassAliases;
@@ -761,6 +857,46 @@ namespace ts {
761857
visitNode(node.right, noDestructuringValue ? visitorNoDestructuringValue : visitor, isExpression)
762858
);
763859
}
860+
else if (isAssignmentExpression(node) && isPropertyAccessExpression(node.left) && isPrivateName(node.left.name)) {
861+
const privateNameInfo = accessPrivateName(node.left.name);
862+
if (privateNameInfo && privateNameInfo.type === PrivateNameType.InstanceField) {
863+
if (isCompoundAssignment(node.operatorToken.kind)) {
864+
const isReceiverInlineable = isSimpleInlineableExpression(node.left.expression);
865+
const getReceiver = isReceiverInlineable ? node.left.expression : createTempVariable(hoistVariableDeclaration);
866+
const setReceiver = isReceiverInlineable
867+
? node.left.expression
868+
: createAssignment(getReceiver, node.left.expression);
869+
return setOriginalNode(
870+
createClassPrivateFieldSetHelper(
871+
context,
872+
setReceiver,
873+
privateNameInfo.weakMapName,
874+
createBinary(
875+
createClassPrivateFieldGetHelper(
876+
context,
877+
getReceiver,
878+
privateNameInfo.weakMapName
879+
),
880+
getOperatorForCompoundAssignment(node.operatorToken.kind),
881+
visitNode(node.right, visitor)
882+
)
883+
),
884+
node
885+
);
886+
}
887+
else {
888+
return setOriginalNode(
889+
createClassPrivateFieldSetHelper(
890+
context,
891+
node.left.expression,
892+
privateNameInfo.weakMapName,
893+
visitNode(node.right, visitor)
894+
),
895+
node
896+
);
897+
}
898+
}
899+
}
764900
return visitEachChild(node, visitor, context);
765901
}
766902

@@ -1587,4 +1723,26 @@ namespace ts {
15871723
location
15881724
);
15891725
}
1726+
1727+
const classPrivateFieldGetHelper: EmitHelper = {
1728+
name: "typescript:classPrivateFieldGet",
1729+
scoped: false,
1730+
text: `var _classPrivateFieldGet = function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return privateMap.get(receiver); };`
1731+
};
1732+
1733+
function createClassPrivateFieldGetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier) {
1734+
context.requestEmitHelper(classPrivateFieldGetHelper);
1735+
return createCall(getHelperName("_classPrivateFieldGet"), /* typeArguments */ undefined, [receiver, privateField]);
1736+
}
1737+
1738+
const classPrivateFieldSetHelper: EmitHelper = {
1739+
name: "typescript:classPrivateFieldSet",
1740+
scoped: false,
1741+
text: `var _classPrivateFieldSet = function (receiver, privateMap, value) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to set private field on non-instance"); } privateMap.set(receiver, value); return value; };`
1742+
};
1743+
1744+
function createClassPrivateFieldSetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier, value: Expression) {
1745+
context.requestEmitHelper(classPrivateFieldSetHelper);
1746+
return createCall(getHelperName("_classPrivateFieldSet"), /* typeArguments */ undefined, [receiver, privateField, value]);
1747+
}
15901748
}

src/compiler/transformers/generators.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -667,28 +667,6 @@ namespace ts {
667667
}
668668
}
669669

670-
function isCompoundAssignment(kind: BinaryOperator): kind is CompoundAssignmentOperator {
671-
return kind >= SyntaxKind.FirstCompoundAssignment
672-
&& kind <= SyntaxKind.LastCompoundAssignment;
673-
}
674-
675-
function getOperatorForCompoundAssignment(kind: CompoundAssignmentOperator): BitwiseOperatorOrHigher {
676-
switch (kind) {
677-
case SyntaxKind.PlusEqualsToken: return SyntaxKind.PlusToken;
678-
case SyntaxKind.MinusEqualsToken: return SyntaxKind.MinusToken;
679-
case SyntaxKind.AsteriskEqualsToken: return SyntaxKind.AsteriskToken;
680-
case SyntaxKind.AsteriskAsteriskEqualsToken: return SyntaxKind.AsteriskAsteriskToken;
681-
case SyntaxKind.SlashEqualsToken: return SyntaxKind.SlashToken;
682-
case SyntaxKind.PercentEqualsToken: return SyntaxKind.PercentToken;
683-
case SyntaxKind.LessThanLessThanEqualsToken: return SyntaxKind.LessThanLessThanToken;
684-
case SyntaxKind.GreaterThanGreaterThanEqualsToken: return SyntaxKind.GreaterThanGreaterThanToken;
685-
case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: return SyntaxKind.GreaterThanGreaterThanGreaterThanToken;
686-
case SyntaxKind.AmpersandEqualsToken: return SyntaxKind.AmpersandToken;
687-
case SyntaxKind.BarEqualsToken: return SyntaxKind.BarToken;
688-
case SyntaxKind.CaretEqualsToken: return SyntaxKind.CaretToken;
689-
}
690-
}
691-
692670
/**
693671
* Visits a right-associative binary expression containing `yield`.
694672
*

src/compiler/transformers/utilities.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,4 +335,26 @@ namespace ts {
335335
return member.kind === SyntaxKind.PropertyDeclaration
336336
&& (<PropertyDeclaration>member).initializer !== undefined;
337337
}
338+
339+
export function isCompoundAssignment(kind: BinaryOperator): kind is CompoundAssignmentOperator {
340+
return kind >= SyntaxKind.FirstCompoundAssignment
341+
&& kind <= SyntaxKind.LastCompoundAssignment;
342+
}
343+
344+
export function getOperatorForCompoundAssignment(kind: CompoundAssignmentOperator): BitwiseOperatorOrHigher {
345+
switch (kind) {
346+
case SyntaxKind.PlusEqualsToken: return SyntaxKind.PlusToken;
347+
case SyntaxKind.MinusEqualsToken: return SyntaxKind.MinusToken;
348+
case SyntaxKind.AsteriskEqualsToken: return SyntaxKind.AsteriskToken;
349+
case SyntaxKind.AsteriskAsteriskEqualsToken: return SyntaxKind.AsteriskAsteriskToken;
350+
case SyntaxKind.SlashEqualsToken: return SyntaxKind.SlashToken;
351+
case SyntaxKind.PercentEqualsToken: return SyntaxKind.PercentToken;
352+
case SyntaxKind.LessThanLessThanEqualsToken: return SyntaxKind.LessThanLessThanToken;
353+
case SyntaxKind.GreaterThanGreaterThanEqualsToken: return SyntaxKind.GreaterThanGreaterThanToken;
354+
case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: return SyntaxKind.GreaterThanGreaterThanGreaterThanToken;
355+
case SyntaxKind.AmpersandEqualsToken: return SyntaxKind.AmpersandToken;
356+
case SyntaxKind.BarEqualsToken: return SyntaxKind.BarToken;
357+
case SyntaxKind.CaretEqualsToken: return SyntaxKind.CaretToken;
358+
}
359+
}
338360
}

src/compiler/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1757,6 +1757,11 @@ namespace ts {
17571757
name: Identifier | PrivateName;
17581758
}
17591759

1760+
/*@internal*/
1761+
export interface PrivateNamedPropertyAccessExpression extends PropertyAccessExpression {
1762+
name: PrivateName;
1763+
}
1764+
17601765
export interface SuperPropertyAccessExpression extends PropertyAccessExpression {
17611766
expression: SuperExpression;
17621767
}

src/compiler/utilities.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6116,6 +6116,11 @@ namespace ts {
61166116
return isPropertyDeclaration(node) && isPrivateName(node.name);
61176117
}
61186118

6119+
/*@internal*/
6120+
export function isPrivateNamedPropertyAccessExpression(node: Node): node is PrivateNamedPropertyAccessExpression {
6121+
return isPropertyAccessExpression(node) && isPrivateName(node.name);
6122+
}
6123+
61196124
export function isBindingName(node: Node): node is BindingName {
61206125
const kind = node.kind;
61216126
return kind === SyntaxKind.Identifier

tests/baselines/reference/privateNameField.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ class A {
77
}
88

99
//// [privateNameField.js]
10+
var _classPrivateFieldSet = function (receiver, privateMap, value) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to set private field on non-instance"); } privateMap.set(receiver, value); return value; };
1011
var _name;
1112
var A = /** @class */ (function () {
1213
function A(name) {
1314
_name.set(this, void 0);
14-
this.#name = name;
15+
_classPrivateFieldSet(this, _name, name);
1516
}
1617
return A;
1718
}());
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//// [privateNameFieldAccess.ts]
2+
class A {
3+
#myField = "hello world";
4+
constructor() {
5+
console.log(this.#myField);
6+
}
7+
}
8+
9+
10+
//// [privateNameFieldAccess.js]
11+
var _classPrivateFieldGet = function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return privateMap.get(receiver); };
12+
var _myField;
13+
var A = /** @class */ (function () {
14+
function A() {
15+
_myField.set(this, "hello world");
16+
console.log(_classPrivateFieldGet(this, _myField));
17+
}
18+
return A;
19+
}());
20+
_myField = new WeakMap();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
=== tests/cases/conformance/classes/members/privateNames/privateNameFieldAccess.ts ===
2+
class A {
3+
>A : Symbol(A, Decl(privateNameFieldAccess.ts, 0, 0))
4+
5+
#myField = "hello world";
6+
>#myField : Symbol(A.#myField, Decl(privateNameFieldAccess.ts, 0, 9))
7+
8+
constructor() {
9+
console.log(this.#myField);
10+
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
11+
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
12+
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
13+
>this.#myField : Symbol(A.#myField, Decl(privateNameFieldAccess.ts, 0, 9))
14+
>this : Symbol(A, Decl(privateNameFieldAccess.ts, 0, 0))
15+
}
16+
}
17+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
=== tests/cases/conformance/classes/members/privateNames/privateNameFieldAccess.ts ===
2+
class A {
3+
>A : A
4+
5+
#myField = "hello world";
6+
>#myField : string
7+
>"hello world" : "hello world"
8+
9+
constructor() {
10+
console.log(this.#myField);
11+
>console.log(this.#myField) : void
12+
>console.log : (message?: any, ...optionalParams: any[]) => void
13+
>console : Console
14+
>log : (message?: any, ...optionalParams: any[]) => void
15+
>this.#myField : string
16+
>this : this
17+
}
18+
}
19+

0 commit comments

Comments
 (0)