Skip to content

Commit 0174c3c

Browse files
Max Heibermheiber
Max Heiber
authored andcommitted
transform private methods
1 parent 3090a29 commit 0174c3c

File tree

1 file changed

+181
-37
lines changed

1 file changed

+181
-37
lines changed

src/compiler/transformers/esnext.ts

Lines changed: 181 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,26 @@ namespace ts {
1313
/**
1414
* A mapping of private names to information needed for transformation.
1515
*/
16-
type PrivateNameEnvironment = UnderscoreEscapedMap<PrivateNamedInstanceField>;
16+
type PrivateNameEnvironment = UnderscoreEscapedMap<PrivateNamedInstanceField | PrivateNamedInstanceMethod>;
1717

1818
/**
1919
* Identifies the type of private name.
2020
*/
21-
const enum PrivateNameType {
22-
InstanceField
21+
const enum PrivateNamePlacement {
22+
InstanceField,
23+
InstanceMethod
2324
}
2425

2526
interface PrivateNamedInstanceField {
26-
type: PrivateNameType.InstanceField;
27-
weakMapName: Identifier;
27+
placement: PrivateNamePlacement.InstanceField;
28+
accumulator: Identifier;
29+
}
30+
31+
interface PrivateNamedInstanceMethod {
32+
placement: PrivateNamePlacement.InstanceMethod;
33+
accumulator: Identifier;
34+
origFunc: MethodDeclaration;
35+
funcName: Identifier;
2836
}
2937

3038
export function transformESNext(context: TransformationContext) {
@@ -365,11 +373,30 @@ namespace ts {
365373

366374
function transformClassMembers(node: ClassDeclaration | ClassExpression, isDerivedClass: boolean) {
367375
// Declare private names.
368-
const privateProperties = filter(node.members, isPrivatePropertyDeclaration);
369-
privateProperties.forEach(property => addPrivateNameToEnvironment(property.name));
370-
376+
const privateNamedMembers = node.members
377+
.filter(element => isNamedDeclaration(element) && isPrivateName(element.name));
378+
privateNamedMembers.forEach(addPrivateName);
379+
380+
pendingExpressions = pendingExpressions || [];
381+
last(privateNameEnvironmentStack).forEach(entry => {
382+
const { placement } = entry;
383+
switch (placement) {
384+
case PrivateNamePlacement.InstanceField:
385+
break;
386+
case PrivateNamePlacement.InstanceMethod:
387+
entry = entry as PrivateNamedInstanceMethod;
388+
const func = privateNamedMethodToFunction(entry.origFunc, entry.funcName, entry.accumulator);
389+
(pendingExpressions = pendingExpressions || []).push(createAssignment(
390+
entry.funcName,
391+
func
392+
));
393+
break;
394+
default:
395+
Debug.assertNever(placement, "unexpected Private Name Placement");
396+
}
397+
});
371398
const members: ClassElement[] = [];
372-
const constructor = transformConstructor(node, isDerivedClass);
399+
const constructor = transformConstructor(node, isDerivedClass, !!privateNamedMembers.length);
373400
if (constructor) {
374401
members.push(constructor);
375402
}
@@ -378,14 +405,24 @@ namespace ts {
378405
return setTextRange(createNodeArray(members), /*location*/ node.members);
379406
}
380407

381-
function transformConstructor(node: ClassDeclaration | ClassExpression, isDerivedClass: boolean) {
408+
function transformConstructor(node: ClassDeclaration | ClassExpression, isDerivedClass: boolean, declaresPrivateNames: boolean) {
382409
const constructor = visitNode(getFirstConstructorWithBody(node), visitor, isConstructorDeclaration);
383410
const containsPropertyInitializer = forEach(node.members, isInitializedProperty);
384-
if (!containsPropertyInitializer) {
385-
return constructor;
411+
let body = constructor ? constructor.body : undefined;
412+
let parameters = constructor ? constructor.parameters : undefined;
413+
const shouldTransformConstructorBody = containsPropertyInitializer || declaresPrivateNames;
414+
if (shouldTransformConstructorBody) {
415+
if (containsPropertyInitializer) {
416+
parameters = visitParameterList(constructor ? constructor.parameters : undefined, visitor, context);
417+
}
418+
else {
419+
// provide a scope for hoisted declarations for WeakSet or WeakMap for private name brand checks
420+
// not needed if `visitParameterList` was called, because that starts/suspends the lexical environment
421+
context.startLexicalEnvironment();
422+
context.suspendLexicalEnvironment();
423+
}
424+
body = transformConstructorBody(node, constructor, isDerivedClass, declaresPrivateNames);
386425
}
387-
const parameters = visitParameterList(constructor ? constructor.parameters : undefined, visitor, context);
388-
const body = transformConstructorBody(node, constructor, isDerivedClass);
389426
if (!body) {
390427
return undefined;
391428
}
@@ -395,7 +432,7 @@ namespace ts {
395432
createConstructor(
396433
/*decorators*/ undefined,
397434
/*modifiers*/ undefined,
398-
parameters,
435+
parameters || [],
399436
body
400437
),
401438
constructor || node
@@ -405,18 +442,25 @@ namespace ts {
405442
);
406443
}
407444

408-
function transformConstructorBody(node: ClassDeclaration | ClassExpression, constructor: ConstructorDeclaration | undefined, isDerivedClass: boolean) {
445+
function transformConstructorBody(
446+
node: ClassDeclaration | ClassExpression,
447+
constructor: ConstructorDeclaration | undefined,
448+
isDerivedClass: boolean,
449+
classDeclaresPrivateNames: boolean,
450+
) {
409451
const properties = filter(node.members, (node): node is PropertyDeclaration => isPropertyDeclaration(node) && !hasStaticModifier(node));
410452

411-
// Only generate synthetic constructor when there are property declarations to move.
412-
if (!constructor && !some(properties)) {
453+
// Only generate synthetic constructor when there are property or private name declarations to move
454+
if (!constructor && !some(properties) && !classDeclaresPrivateNames) {
413455
return visitFunctionBody(/*node*/ undefined, visitor, context);
414456
}
415457

458+
let statements: Statement[] = [];
459+
460+
// was suspended to provide a scope for parameter properties and/or private names
416461
resumeLexicalEnvironment();
417462

418463
let indexOfFirstStatement = 0;
419-
let statements: Statement[] = [];
420464

421465
if (!constructor && isDerivedClass) {
422466
// Add a synthetic `super` call:
@@ -449,6 +493,26 @@ namespace ts {
449493
// }
450494
//
451495
addInitializedPropertyStatements(statements, properties, createThis());
496+
if (classDeclaresPrivateNames) {
497+
last(privateNameEnvironmentStack).forEach(({ placement, accumulator }) => {
498+
switch (placement) {
499+
case PrivateNamePlacement.InstanceField:
500+
// TODO: instance field add accumulator
501+
break;
502+
case PrivateNamePlacement.InstanceMethod:
503+
statements.push(
504+
createExpressionStatement(
505+
createCall(
506+
createPropertyAccess(accumulator, "add"),
507+
/* typeArguments */ undefined,
508+
[createThis()]
509+
)
510+
)
511+
);
512+
break;
513+
}
514+
});
515+
}
452516

453517
// Add existing statements, skipping the initial super call.
454518
if (constructor) {
@@ -528,10 +592,10 @@ namespace ts {
528592
if (isPrivateName(propertyName)) {
529593
const privateNameInfo = accessPrivateName(propertyName);
530594
if (privateNameInfo) {
531-
switch (privateNameInfo.type) {
532-
case PrivateNameType.InstanceField: {
595+
switch (privateNameInfo.placement) {
596+
case PrivateNamePlacement.InstanceField: {
533597
return createCall(
534-
createPropertyAccess(privateNameInfo.weakMapName, "set"),
598+
createPropertyAccess(privateNameInfo.accumulator, "set"),
535599
/*typeArguments*/ undefined,
536600
[receiver, initializer || createVoidZero()]
537601
);
@@ -557,17 +621,66 @@ namespace ts {
557621
privateNameEnvironmentStack.pop();
558622
}
559623

560-
function addPrivateNameToEnvironment(name: PrivateName) {
624+
function privateNamedMethodToFunction (declaration: MethodDeclaration, funcName: Identifier, accumulator: Identifier): FunctionExpression {
625+
const params = declaration.parameters;
626+
let body = getMutableClone(declaration.body || createBlock([], true));
627+
body = visitEachChild(body, visitor, context);
628+
const toPrepend = startOnNewLine(
629+
createStatement(
630+
createClassPrivateNamedCallCheckHelper(context, accumulator)
631+
)
632+
);
633+
body.statements = setTextRange(
634+
createNodeArray([
635+
toPrepend,
636+
...body.statements
637+
]),
638+
body.statements
639+
);
640+
const func = createFunctionExpression(
641+
/* modifiers */ undefined,
642+
/* asteriskToken */ undefined,
643+
funcName,
644+
/* typeParameters */ undefined,
645+
params,
646+
/* type */ undefined,
647+
body);
648+
return func;
649+
}
650+
651+
652+
function addPrivateName(element: ClassElement & { name: PrivateName }) {
561653
const env = last(privateNameEnvironmentStack);
562-
const text = getTextOfPropertyName(name) as string;
563-
const weakMapName = createFileLevelUniqueName("_" + text.substring(1));
564-
hoistVariableDeclaration(weakMapName);
565-
env.set(name.escapedText, { type: PrivateNameType.InstanceField, weakMapName });
566-
(pendingExpressions || (pendingExpressions = [])).push(
654+
const text = getTextOfPropertyName(element.name) as string;
655+
const accumulator = createFileLevelUniqueName(`_${text.substring(1)}Private`);
656+
const { escapedText } = element.name;
657+
hoistVariableDeclaration(accumulator);
658+
659+
let identifierName: string;
660+
if (hasModifier(element, ModifierFlags.Static)) {
661+
// statics not supported yet
662+
return;
663+
}
664+
if (isPropertyDeclaration(element)) {
665+
identifierName = "WeakMap";
666+
env.set(escapedText, { placement: PrivateNamePlacement.InstanceField, accumulator });
667+
}
668+
else if (isMethodDeclaration(element)) {
669+
identifierName = "WeakSet";
670+
const escapedText = element.name.escapedText;
671+
const escapedTextNoHash = `_${`${escapedText}`.slice(1)}`;
672+
const funcName: Identifier = createFileLevelUniqueName(escapedTextNoHash);
673+
env.set(escapedText, { placement: PrivateNamePlacement.InstanceMethod, accumulator, funcName, origFunc: element });
674+
hoistVariableDeclaration(funcName); // todo: hoist in lexical, not func scope
675+
}
676+
else {
677+
return;
678+
}
679+
(pendingExpressions = pendingExpressions || []).push(
567680
createAssignment(
568-
weakMapName,
681+
accumulator,
569682
createNew(
570-
createIdentifier("WeakMap"),
683+
createIdentifier(identifierName),
571684
/*typeArguments*/ undefined,
572685
[]
573686
)
@@ -589,14 +702,14 @@ namespace ts {
589702
if (isPrivateName(node.name)) {
590703
const privateNameInfo = accessPrivateName(node.name);
591704
if (privateNameInfo) {
592-
switch (privateNameInfo.type) {
593-
case PrivateNameType.InstanceField:
705+
switch (privateNameInfo.placement) {
706+
case PrivateNamePlacement.InstanceField:
594707
return setOriginalNode(
595708
setTextRange(
596709
createClassPrivateFieldGetHelper(
597710
context,
598711
visitNode(node.expression, visitor, isExpression),
599-
privateNameInfo.weakMapName
712+
privateNameInfo.accumulator
600713
),
601714
node
602715
),
@@ -622,6 +735,23 @@ namespace ts {
622735
);
623736
receiver = generatedName;
624737
}
738+
const privateNameEntry = last(privateNameEnvironmentStack).get(node.expression.name.escapedText);
739+
if (privateNameEntry && privateNameEntry.placement === PrivateNamePlacement.InstanceMethod) {
740+
return setOriginalNode(
741+
setTextRange(
742+
createCall(
743+
createPropertyAccess(
744+
privateNameEntry.funcName,
745+
"call"
746+
),
747+
/*typeArguments*/ undefined,
748+
[createThis(), ...node.arguments]
749+
),
750+
/* location */ node
751+
),
752+
node
753+
);
754+
}
625755
return visitNode(
626756
updateCall(
627757
node,
@@ -903,7 +1033,7 @@ namespace ts {
9031033
}
9041034
else if (isAssignmentExpression(node) && isPropertyAccessExpression(node.left) && isPrivateName(node.left.name)) {
9051035
const privateNameInfo = accessPrivateName(node.left.name);
906-
if (privateNameInfo && privateNameInfo.type === PrivateNameType.InstanceField) {
1036+
if (privateNameInfo && privateNameInfo.placement === PrivateNamePlacement.InstanceField) {
9071037
if (isCompoundAssignment(node.operatorToken.kind)) {
9081038
const isReceiverInlineable = isSimpleInlineableExpression(node.left.expression);
9091039
const getReceiver = isReceiverInlineable ? node.left.expression : createTempVariable(hoistVariableDeclaration);
@@ -914,12 +1044,12 @@ namespace ts {
9141044
createClassPrivateFieldSetHelper(
9151045
context,
9161046
setReceiver,
917-
privateNameInfo.weakMapName,
1047+
privateNameInfo.accumulator,
9181048
createBinary(
9191049
createClassPrivateFieldGetHelper(
9201050
context,
9211051
getReceiver,
922-
privateNameInfo.weakMapName
1052+
privateNameInfo.accumulator
9231053
),
9241054
getOperatorForCompoundAssignment(node.operatorToken.kind),
9251055
visitNode(node.right, visitor)
@@ -933,7 +1063,7 @@ namespace ts {
9331063
createClassPrivateFieldSetHelper(
9341064
context,
9351065
node.left.expression,
936-
privateNameInfo.weakMapName,
1066+
privateNameInfo.accumulator,
9371067
visitNode(node.right, visitor)
9381068
),
9391069
node
@@ -1252,6 +1382,9 @@ namespace ts {
12521382
function visitMethodDeclaration(node: MethodDeclaration) {
12531383
const savedEnclosingFunctionFlags = enclosingFunctionFlags;
12541384
enclosingFunctionFlags = getFunctionFlags(node);
1385+
if (isPrivateName(node.name)) {
1386+
return [];
1387+
}
12551388
const updated = updateMethod(
12561389
node,
12571390
/*decorators*/ undefined,
@@ -1789,4 +1922,15 @@ namespace ts {
17891922
context.requestEmitHelper(classPrivateFieldSetHelper);
17901923
return createCall(getHelperName("_classPrivateFieldSet"), /* typeArguments */ undefined, [receiver, privateField, value]);
17911924
}
1925+
const classPrivateNamedCallCheckHelper: EmitHelper = {
1926+
name: "typescript:classPrivateNamedCallCheck",
1927+
scoped: false,
1928+
text: `var _classPrivateNamedCallCheck = function (receiver, privateSet) { if (!privateSet.has(receiver)) { throw new TypeError("attempted to get weak field on non-instance"); }};`
1929+
};
1930+
1931+
function createClassPrivateNamedCallCheckHelper(context: TransformationContext, weakSet: Identifier) {
1932+
context.requestEmitHelper(classPrivateNamedCallCheckHelper);
1933+
return createCall(getHelperName("_classPrivateNamedCallCheck"), /* typeArguments */ undefined, [ createThis(), weakSet ]);
1934+
}
1935+
17921936
}

0 commit comments

Comments
 (0)