Skip to content

Commit 75c68a1

Browse files
author
Max Heiber
committed
private named methods
1 parent 3090a29 commit 75c68a1

File tree

1 file changed

+179
-36
lines changed

1 file changed

+179
-36
lines changed

src/compiler/transformers/esnext.ts

Lines changed: 179 additions & 36 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,23 @@ 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.body;
412+
let parameters = constructor.parameters;
413+
if (containsPropertyInitializer || declaresPrivateNames) {
414+
if (containsPropertyInitializer) {
415+
parameters = visitParameterList(constructor ? constructor.parameters : undefined, visitor, context);
416+
}
417+
else {
418+
// provide a scope for hoisted declarations for WeakSet or WeakMap for private name brand checks
419+
// not needed if `visitParameterList` was called, because that starts/suspends the lexical environment
420+
context.startLexicalEnvironment();
421+
context.suspendLexicalEnvironment();
422+
}
423+
body = transformConstructorBody(node, constructor, isDerivedClass, declaresPrivateNames);
386424
}
387-
const parameters = visitParameterList(constructor ? constructor.parameters : undefined, visitor, context);
388-
const body = transformConstructorBody(node, constructor, isDerivedClass);
389425
if (!body) {
390426
return undefined;
391427
}
@@ -405,18 +441,25 @@ namespace ts {
405441
);
406442
}
407443

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

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

457+
let statements: Statement[] = [];
458+
459+
// was suspended if/when parameters were visited
416460
resumeLexicalEnvironment();
417461

418462
let indexOfFirstStatement = 0;
419-
let statements: Statement[] = [];
420463

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

453516
// Add existing statements, skipping the initial super call.
454517
if (constructor) {
@@ -528,10 +591,10 @@ namespace ts {
528591
if (isPrivateName(propertyName)) {
529592
const privateNameInfo = accessPrivateName(propertyName);
530593
if (privateNameInfo) {
531-
switch (privateNameInfo.type) {
532-
case PrivateNameType.InstanceField: {
594+
switch (privateNameInfo.placement) {
595+
case PrivateNamePlacement.InstanceField: {
533596
return createCall(
534-
createPropertyAccess(privateNameInfo.weakMapName, "set"),
597+
createPropertyAccess(privateNameInfo.accumulator, "set"),
535598
/*typeArguments*/ undefined,
536599
[receiver, initializer || createVoidZero()]
537600
);
@@ -557,17 +620,66 @@ namespace ts {
557620
privateNameEnvironmentStack.pop();
558621
}
559622

560-
function addPrivateNameToEnvironment(name: PrivateName) {
623+
function privateNamedMethodToFunction (declaration: MethodDeclaration, funcName: Identifier, accumulator: Identifier): FunctionExpression {
624+
const params = declaration.parameters;
625+
let body = getMutableClone(declaration.body || createBlock([], true));
626+
body = visitEachChild(body, visitor, context);
627+
const toPrepend = startOnNewLine(
628+
createStatement(
629+
createClassPrivateNamedCallCheckHelper(context, accumulator)
630+
)
631+
);
632+
body.statements = setTextRange(
633+
createNodeArray([
634+
toPrepend,
635+
...body.statements
636+
]),
637+
body.statements
638+
);
639+
const func = createFunctionExpression(
640+
/* modifiers */ undefined,
641+
/* asteriskToken */ undefined,
642+
funcName,
643+
/* typeParameters */ undefined,
644+
params,
645+
/* type */ undefined,
646+
body);
647+
return func;
648+
}
649+
650+
651+
function addPrivateName(element: ClassElement & { name: PrivateName }) {
561652
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(
653+
const text = getTextOfPropertyName(element.name) as string;
654+
const accumulator = createFileLevelUniqueName(`_${text.substring(1)}BrandCheck`);
655+
const { escapedText } = element.name;
656+
hoistVariableDeclaration(accumulator);
657+
658+
let identifierName: string;
659+
if (hasModifier(element, ModifierFlags.Static)) {
660+
// statics not supported yet
661+
return;
662+
}
663+
if (isPropertyDeclaration(element)) {
664+
identifierName = "WeakMap";
665+
env.set(escapedText, { placement: PrivateNamePlacement.InstanceField, accumulator });
666+
}
667+
else if (isMethodDeclaration(element)) {
668+
identifierName = "WeakSet";
669+
const escapedText = element.name.escapedText;
670+
const escapedTextNoHash = `_${`${escapedText}`.slice(1)}`;
671+
const funcName: Identifier = createFileLevelUniqueName(escapedTextNoHash);
672+
env.set(escapedText, { placement: PrivateNamePlacement.InstanceMethod, accumulator, funcName, origFunc: element });
673+
hoistVariableDeclaration(funcName); // todo: hoist in lexical, not func scope
674+
}
675+
else {
676+
return;
677+
}
678+
(pendingExpressions = pendingExpressions || []).push(
567679
createAssignment(
568-
weakMapName,
680+
accumulator,
569681
createNew(
570-
createIdentifier("WeakMap"),
682+
createIdentifier(identifierName),
571683
/*typeArguments*/ undefined,
572684
[]
573685
)
@@ -589,14 +701,14 @@ namespace ts {
589701
if (isPrivateName(node.name)) {
590702
const privateNameInfo = accessPrivateName(node.name);
591703
if (privateNameInfo) {
592-
switch (privateNameInfo.type) {
593-
case PrivateNameType.InstanceField:
704+
switch (privateNameInfo.placement) {
705+
case PrivateNamePlacement.InstanceField:
594706
return setOriginalNode(
595707
setTextRange(
596708
createClassPrivateFieldGetHelper(
597709
context,
598710
visitNode(node.expression, visitor, isExpression),
599-
privateNameInfo.weakMapName
711+
privateNameInfo.accumulator
600712
),
601713
node
602714
),
@@ -622,6 +734,23 @@ namespace ts {
622734
);
623735
receiver = generatedName;
624736
}
737+
const privateNameEntry = last(privateNameEnvironmentStack).get(node.expression.name.escapedText);
738+
if (privateNameEntry && privateNameEntry.placement === PrivateNamePlacement.InstanceMethod) {
739+
return setOriginalNode(
740+
setTextRange(
741+
createCall(
742+
createPropertyAccess(
743+
privateNameEntry.funcName,
744+
"call"
745+
),
746+
/*typeArguments*/ undefined,
747+
[createThis(), ...node.arguments]
748+
),
749+
/* location */ node
750+
),
751+
node
752+
);
753+
}
625754
return visitNode(
626755
updateCall(
627756
node,
@@ -903,7 +1032,7 @@ namespace ts {
9031032
}
9041033
else if (isAssignmentExpression(node) && isPropertyAccessExpression(node.left) && isPrivateName(node.left.name)) {
9051034
const privateNameInfo = accessPrivateName(node.left.name);
906-
if (privateNameInfo && privateNameInfo.type === PrivateNameType.InstanceField) {
1035+
if (privateNameInfo && privateNameInfo.placement === PrivateNamePlacement.InstanceField) {
9071036
if (isCompoundAssignment(node.operatorToken.kind)) {
9081037
const isReceiverInlineable = isSimpleInlineableExpression(node.left.expression);
9091038
const getReceiver = isReceiverInlineable ? node.left.expression : createTempVariable(hoistVariableDeclaration);
@@ -914,12 +1043,12 @@ namespace ts {
9141043
createClassPrivateFieldSetHelper(
9151044
context,
9161045
setReceiver,
917-
privateNameInfo.weakMapName,
1046+
privateNameInfo.accumulator,
9181047
createBinary(
9191048
createClassPrivateFieldGetHelper(
9201049
context,
9211050
getReceiver,
922-
privateNameInfo.weakMapName
1051+
privateNameInfo.accumulator
9231052
),
9241053
getOperatorForCompoundAssignment(node.operatorToken.kind),
9251054
visitNode(node.right, visitor)
@@ -933,7 +1062,7 @@ namespace ts {
9331062
createClassPrivateFieldSetHelper(
9341063
context,
9351064
node.left.expression,
936-
privateNameInfo.weakMapName,
1065+
privateNameInfo.accumulator,
9371066
visitNode(node.right, visitor)
9381067
),
9391068
node
@@ -1252,6 +1381,9 @@ namespace ts {
12521381
function visitMethodDeclaration(node: MethodDeclaration) {
12531382
const savedEnclosingFunctionFlags = enclosingFunctionFlags;
12541383
enclosingFunctionFlags = getFunctionFlags(node);
1384+
if (isPrivateName(node.name)) {
1385+
return [];
1386+
}
12551387
const updated = updateMethod(
12561388
node,
12571389
/*decorators*/ undefined,
@@ -1789,4 +1921,15 @@ namespace ts {
17891921
context.requestEmitHelper(classPrivateFieldSetHelper);
17901922
return createCall(getHelperName("_classPrivateFieldSet"), /* typeArguments */ undefined, [receiver, privateField, value]);
17911923
}
1924+
const classPrivateNamedCallCheckHelper: EmitHelper = {
1925+
name: "typescript:classPrivateNamedCallCheck",
1926+
scoped: false,
1927+
text: `var _classPrivateNamedCallCheck = function (receiver, privateSet) { if (!privateSet.has(receiver)) { throw new TypeError("attempted to get weak field on non-instance"); }};`
1928+
};
1929+
1930+
function createClassPrivateNamedCallCheckHelper(context: TransformationContext, weakSet: Identifier) {
1931+
context.requestEmitHelper(classPrivateNamedCallCheckHelper);
1932+
return createCall(getHelperName("_classPrivateNamedCallCheck"), /* typeArguments */ undefined, [ createThis(), weakSet ]);
1933+
}
1934+
17921935
}

0 commit comments

Comments
 (0)