diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 154416b78567c..f5b26cb406793 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -3025,6 +3025,16 @@ namespace ts { return node; } + export function replaceNode(original: Node | undefined, node: T) : T { + return setOriginalNode( + setTextRange( + node, + /* location */ original + ), + original + ); + } + function mergeEmitNode(sourceEmitNode: EmitNode, destEmitNode: EmitNode | undefined) { const { flags, diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index 3c3234b174817..8508d28bafd5a 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -27,15 +27,27 @@ namespace ts { let enclosingSuperContainerFlags: NodeCheckFlags = 0; + const enum PrivateNamePlacement { InstanceField, InstanceMethod }; + /** - * Maps private names to the generated name of the WeakMap. + * Maps private names to the generated name and (if applicable) accessor */ interface PrivateNameEnvironment { - [name: string]: { - weakMap: Identifier; - initializer?: Expression; - }; + [name: string]: PrivateNamedInstanceFieldEntry | PrivateNamedInstanceMethodEntry; + } + + interface PrivateNamedInstanceFieldEntry { + placement: PrivateNamePlacement.InstanceField; + accumulator: Identifier; + initializer?: Expression; + } + + interface PrivateNamedInstanceMethodEntry { + placement: PrivateNamePlacement.InstanceMethod; + accumulator: Identifier; + func: FunctionDeclaration & {name: Identifier}; } + const privateNameEnvironmentStack: PrivateNameEnvironment[] = []; let privateNameEnvironmentIndex = -1; @@ -120,6 +132,8 @@ namespace ts { return visitClassDeclaration(node as ClassDeclaration); case SyntaxKind.ClassExpression: return visitClassExpression(node as ClassExpression); + case SyntaxKind.CallExpression: + return visitCallExpression(node as CallExpression); default: return visitEachChild(node, visitor, context); } @@ -129,50 +143,131 @@ namespace ts { return privateNameEnvironmentStack[privateNameEnvironmentIndex]; } - function addPrivateName(name: PrivateName, initializer?: Expression) { + function addPrivateName(declaration: PrivateNamedDeclaration, initializer?: Expression): void { const environment = currentPrivateNameEnvironment(); - const nameString = name.escapedText as string; + const nameString = declaration.name.escapedText as string; if (nameString in environment) { throw new Error("Redeclaring private name " + nameString + "."); } - const weakMap = createFileLevelUniqueName("_" + nameString.substring(1)); - environment[nameString] = { - weakMap, - initializer - }; - return weakMap; + const accumulator = createFileLevelUniqueName(`_accumulator_${nameString.slice(1)}`); + if (declaration.kind === SyntaxKind.PropertyDeclaration) { + if (hasModifier(declaration, ModifierFlags.Static)) { + // todo: static property + return; + } + else { + environment[nameString] = { + placement: PrivateNamePlacement.InstanceField, + accumulator: accumulator, + initializer + }; + } + } + else if (isMethodDeclaration(declaration)) { + if (hasModifier(declaration, ModifierFlags.Static)) { + // todo: static method + return; + } + else if (!declaration.body) { + return; + } + else { + const params = declaration.parameters; + const body = getMutableClone(declaration.body); + const receiver = createIdentifier("receiver"); + const toPrepend = startOnNewLine( + createStatement( + createClassPrivateNamedCallCheckHelper(context, receiver, accumulator) + ) + ); + body.statements = setTextRange( + createNodeArray([ + toPrepend, + ...body.statements + ]), + body.statements + ); + const funcName = createFileLevelUniqueName(`_${nameString.slice(1)}`); + const func = createFunctionDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, + /* asteriskToken */ undefined, + funcName, + /* typeParameters */ undefined, + [ + createParameter(undefined, undefined, undefined, receiver), + ...params + ], + /* type */ undefined, + body) as FunctionDeclaration & {name: Identifier}; + environment[nameString] = { + placement: PrivateNamePlacement.InstanceMethod, + accumulator, + func + }; + } + } } - function accessPrivateName(name: PrivateName) { + function getPrivateNameRecord(name: PrivateName) { const environment = currentPrivateNameEnvironment(); const nameString = name.escapedText as string; if (nameString in environment) { - return environment[nameString].weakMap; + return environment[nameString]; } // Undeclared private name. return undefined; } + function visitCallExpression(node: CallExpression): Expression { + if (isPropertyAccessExpression(node.expression) + && isPrivateName(node.expression.name)) { + const entry = getPrivateNameRecord(node.expression.name); + if (!entry) { + return node; + } + const receiver = node.expression.expression; + const { placement } = entry; + if (placement === PrivateNamePlacement.InstanceMethod) { + const { func } = entry as PrivateNamedInstanceMethodEntry; + return replaceNode( + node, + createCall(func.name, undefined, [receiver, ...node.arguments]) + ); + + } + } + return visitEachChild(node, visitor, context); + } + function visitPropertyAccessExpression(node: PropertyAccessExpression): Expression { if (isPrivateName(node.name)) { - const weakMapName = accessPrivateName(node.name); - if (!weakMapName) { + const entry = getPrivateNameRecord(node.name); + if (!entry) { return node; } - return setOriginalNode( - setTextRange( - createClassPrivateFieldGetHelper(context, node.expression, weakMapName), - /* location */ node - ), - node - ); + const { placement, accumulator } = entry; + if (placement === PrivateNamePlacement.InstanceField) { + return replaceNode( + node, + createClassPrivateFieldGetHelper(context, node.expression, accumulator), + ); + } } return visitEachChild(node, visitor, context); } function visitorCollectPrivateNames(node: Node): VisitResult { - if (isPrivateNamedPropertyDeclaration(node)) { - addPrivateName(node.name); + if ( + isPrivateNamedDeclaration(node) + && (isPropertyDeclaration(node) + || isMethodDeclaration(node) + // TODO: getters/setters + ) + // TODO: statics + && !hasModifier(node, ModifierFlags.Static) + ) { + addPrivateName(node); return undefined; } // Don't collect private names from nested classes. @@ -186,7 +281,7 @@ namespace ts { startPrivateNameEnvironment(); node = visitEachChild(node, visitorCollectPrivateNames, context); node = visitEachChild(node, visitor, context); - const statements = createPrivateNameWeakMapDeclarations( + const statements = createPrivateNameaccumulatorDeclarations( currentPrivateNameEnvironment() ); if (statements.length) { @@ -199,6 +294,15 @@ namespace ts { node.heritageClauses, transformClassMembers(node.members) ); + Object.keys( + currentPrivateNameEnvironment() + ) + .map(key => currentPrivateNameEnvironment()[key]) + .filter(x => x.placement === PrivateNamePlacement.InstanceMethod) + .forEach(e => { + const entry = e as PrivateNamedInstanceMethodEntry; + statements.push(entry.func) + }); } statements.unshift(node); endPrivateNameEnvironment(); @@ -209,7 +313,7 @@ namespace ts { startPrivateNameEnvironment(); node = visitEachChild(node, visitorCollectPrivateNames, context); node = visitEachChild(node, visitor, context); - const expressions = createPrivateNameWeakMapAssignments( + const expressions = createPrivateNameaccumulatorAssignments( currentPrivateNameEnvironment() ); if (expressions.length) { @@ -240,28 +344,44 @@ namespace ts { return privateNameEnvironment; } - function createPrivateNameWeakMapDeclarations(environment: PrivateNameEnvironment): Statement[] { + function createPrivateNameaccumulatorDeclarations(environment: PrivateNameEnvironment): Statement[] { return Object.keys(environment).map(name => { - const privateName = environment[name]; - return createVariableStatement( - /* modifiers */ undefined, - [createVariableDeclaration(privateName.weakMap, - /* typeNode */ undefined, - createNew( - createIdentifier("WeakMap"), - /* typeArguments */ undefined, - /* argumentsArray */ undefined - ))] - ); + const { placement, accumulator } = environment[name]; + switch (placement) { + case PrivateNamePlacement.InstanceField: + return createVariableStatement( + /* modifiers */ undefined, + [createVariableDeclaration(accumulator, + /* typeNode */ undefined, + createNew( + createIdentifier("WeakMap"), + /* typeArguments */ undefined, + /* argumentsArray */ undefined + ))] + ); + case PrivateNamePlacement.InstanceMethod: + return createVariableStatement( + /* modifiers */ undefined, + [createVariableDeclaration(accumulator, + /* typeNode */ undefined, + createNew( + createIdentifier("WeakSet"), + /* typeArguments */ undefined, + /* argumentsArray */ undefined + ))] + ); + default: + return Debug.assertNever(placement); + } }); } - function createPrivateNameWeakMapAssignments(environment: PrivateNameEnvironment): Expression[] { + function createPrivateNameaccumulatorAssignments(environment: PrivateNameEnvironment): Expression[] { return Object.keys(environment).map(name => { - const privateName = environment[name]; - hoistVariableDeclaration(privateName.weakMap); + const { accumulator } = environment[name]; + hoistVariableDeclaration(accumulator); return createBinary( - privateName.weakMap, + accumulator, SyntaxKind.EqualsToken, createNew(createIdentifier("WeakMap"), /* typeArguments */ undefined, /* argumentsArray */ undefined) ); @@ -273,14 +393,31 @@ namespace ts { const privateNameEnvironment = currentPrivateNameEnvironment(); // Initialize private properties. const initializerStatements = Object.keys(privateNameEnvironment).map(name => { - const privateName = privateNameEnvironment[name]; - return createStatement( - createCall( - createPropertyAccess(privateName.weakMap, "set"), - /* typeArguments */ undefined, - [createThis(), privateName.initializer || createVoidZero()] - ) - ); + const entry = privateNameEnvironment[name]; + const { accumulator, placement } = entry; + + switch(placement) { + case PrivateNamePlacement.InstanceField: + return createStatement( + createCall( + createPropertyAccess(accumulator, "set"), + /* typeArguments */ undefined, + [createThis(), (entry as PrivateNamedInstanceFieldEntry).initializer || createVoidZero()] + ) + ); + case PrivateNamePlacement.InstanceMethod: + return createStatement( + createCall( + createPropertyAccess(accumulator, "add"), + /* typeArguments */ undefined, + [createThis()] + ) + ); + default: + return Debug.assertNever(placement); + + } + }); const ctor = find( members, @@ -476,11 +613,12 @@ namespace ts { isPropertyAccessExpression(node.left) && isPrivateName(node.left.name)) { - const weakMapName = accessPrivateName(node.left.name); - if (!weakMapName) { + const privateNameRecord = getPrivateNameRecord(node.left.name); + if (!privateNameRecord) { // Don't change output for undeclared private names (error). return node; } + const accumulatorName = privateNameRecord.accumulator; if (isCompoundAssignment(node.operatorToken.kind)) { let setReceiver: Expression; let getReceiver: Expression; @@ -499,9 +637,9 @@ namespace ts { createClassPrivateFieldSetHelper( context, setReceiver, - weakMapName, + accumulatorName, createBinary( - createClassPrivateFieldGetHelper(context, getReceiver, weakMapName), + createClassPrivateFieldGetHelper(context, getReceiver, accumulatorName), getOperatorForCompoundAssignment(node.operatorToken.kind), visitNode(node.right, visitor) ) @@ -514,7 +652,7 @@ namespace ts { createClassPrivateFieldSetHelper( context, node.left.expression, - weakMapName, + accumulatorName, visitNode(node.right, visitor) ), node @@ -1183,6 +1321,17 @@ namespace ts { return createCall(getHelperName("_classPrivateFieldGet"), /* typeArguments */ undefined, [ receiver, privateField ]); } + const classPrivateNamedCallCheckHelper: EmitHelper = { + name: "typescript:classPrivateNamedCallCheck", + scoped: false, + text: `var _classPrivateNamedCallCheck = function (receiver, privateSet) { if (!privateSet.has(receiver)) { throw new TypeError("attempted to get weak field on non-instance"); }};` + }; + + function createClassPrivateNamedCallCheckHelper(context: TransformationContext, receiver: Expression, weakSet: Identifier) { + context.requestEmitHelper(classPrivateNamedCallCheckHelper); + return createCall(getHelperName("_classPrivateNamedCallCheck"), /* typeArguments */ undefined, [ receiver, weakSet ]); + } + const classPrivateFieldSetHelper: EmitHelper = { name: "typescript:classPrivateFieldSet", scoped: false, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index a86badd2c56a4..87ff3a38a98fb 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -864,7 +864,7 @@ namespace ts { initializer?: Expression; // Optional initializer } - export interface PrivateNamedPropertyDeclaration extends PropertyDeclaration { + export interface PrivateNamedDeclaration extends PropertyDeclaration { name: PrivateName; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 74c1e6086514f..e58c394b91f47 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -6091,8 +6091,8 @@ namespace ts { } } - export function isPrivateNamedPropertyDeclaration(node: Node): node is PrivateNamedPropertyDeclaration { - return node && isPropertyDeclaration(node) && isPrivateName(node.name); + export function isPrivateNamedDeclaration(node: Node): node is PrivateNamedDeclaration { + return node && isNamedDeclaration(node) && isPrivateName(node.name); } // Type members