From 7339eca6901b9d10314a24418c8b937529a7e120 Mon Sep 17 00:00:00 2001 From: Maxwell Heiber Date: Fri, 22 Jun 2018 15:16:06 -0400 Subject: [PATCH 01/19] can parse private names (#1) do a really rough, quick-and-dirty unblock-people job of letting #foo be parse-able. steps to test: npm run lint && npm test See https://bbgithub.dev.bloomberg.com/javascript-guild/typescript-private-fields/ quick start guide for seeing the AST CI testing coming soon --- src/compiler/scanner.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index fdd48f1bea7b6..9d617cd900097 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -1725,10 +1725,7 @@ namespace ts { return token = SyntaxKind.Unknown; case CharacterCodes.hash: pos++; - if ( - languageVersion === ScriptTarget.ESNext - && isIdentifierStart(ch = text.charCodeAt(pos), languageVersion) - ) { + if (isIdentifierStart(ch = text.charCodeAt(pos), languageVersion)) { tokenFlags |= TokenFlags.PrivateName; pos++; while (pos < end && isIdentifierPart(ch = text.charCodeAt(pos), languageVersion)) pos++; @@ -1739,7 +1736,11 @@ namespace ts { return token = SyntaxKind.Identifier; } error(Diagnostics.Invalid_character); +<<<<<<< HEAD // no `pos++` because already advanced at beginning of this `case` statement +======= + pos++; +>>>>>>> can parse private names (#1) return token = SyntaxKind.Unknown; default: if (isIdentifierStart(ch, languageVersion)) { From efd0ada13678284d0908e455412e32ef27e40ee8 Mon Sep 17 00:00:00 2001 From: Maxwell Heiber Date: Sat, 23 Jun 2018 22:06:10 -0400 Subject: [PATCH 02/19] jenkinsfile (#2) * jenkinsfile * add npm run build step * lint * baseline change * accept more baselines * accept baselines * fix-scanner * baselines again --- Jenkinsfile | 25 +++++++++++++++++++++++++ src/compiler/scanner.ts | 9 ++++----- 2 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000000000..4d29d87fd406d --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,25 @@ +node('GNRLD') { + stage('checkout') { + deleteDir() + checkout scm + } + + stage('install') { + sh ''' + npm config set "always-auth" "false"; + npm config set "registry" "http://artprod.dev.bloomberg.com:8080/artifactory/api/npm/npm-repos"; + npm config set "strict-ssl" "false"; + npm i --ignore-scripts -s; + npm run build + ''' + } + + stage('lint') { + sh "npm run lint" + } + + stage('test') { + sh "npm run test" + } +} + diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 9d617cd900097..fdd48f1bea7b6 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -1725,7 +1725,10 @@ namespace ts { return token = SyntaxKind.Unknown; case CharacterCodes.hash: pos++; - if (isIdentifierStart(ch = text.charCodeAt(pos), languageVersion)) { + if ( + languageVersion === ScriptTarget.ESNext + && isIdentifierStart(ch = text.charCodeAt(pos), languageVersion) + ) { tokenFlags |= TokenFlags.PrivateName; pos++; while (pos < end && isIdentifierPart(ch = text.charCodeAt(pos), languageVersion)) pos++; @@ -1736,11 +1739,7 @@ namespace ts { return token = SyntaxKind.Identifier; } error(Diagnostics.Invalid_character); -<<<<<<< HEAD // no `pos++` because already advanced at beginning of this `case` statement -======= - pos++; ->>>>>>> can parse private names (#1) return token = SyntaxKind.Unknown; default: if (isIdentifierStart(ch, languageVersion)) { From 0f567a517f0ad103441029a419173bf046f898ce Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Thu, 28 Jun 2018 14:14:40 -0400 Subject: [PATCH 03/19] Start ES2015 transformation --- src/compiler/transformers/esnext.ts | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index cf1c6c447bee9..e21c0dafd93ba 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -101,11 +101,35 @@ namespace ts { return visitParenthesizedExpression(node as ParenthesizedExpression, noDestructuringValue); case SyntaxKind.CatchClause: return visitCatchClause(node as CatchClause); + case SyntaxKind.PropertyDeclaration: + return visitPropertyDeclaration(node as PropertyDeclaration); + case SyntaxKind.PropertyAccessExpression: + return visitPropertyAccessExpression(node as PropertyAccessExpression); default: return visitEachChild(node, visitor, context); } } + function visitPropertyAccessExpression(node: PropertyAccessExpression): Expression { + if (node.name.isPrivateName) { + return setOriginalNode( + setTextRange( + createClassPrivateFieldGetHelper(context, node.expression, node.name), + /* location */ node + ), + node + ); + } + return visitEachChild(node, visitor, context); + } + + function visitPropertyDeclaration(node: PropertyDeclaration): VisitResult { + if (isIdentifier(node.name) && node.name.isPrivateName) { + createClassPrivateFieldHelper(context, node.name); + } + return visitEachChild(node, visitor, context); + } + function visitAwaitExpression(node: AwaitExpression): Expression { if (enclosingFunctionFlags & FunctionFlags.Async && enclosingFunctionFlags & FunctionFlags.Generator) { return setOriginalNode( @@ -917,6 +941,36 @@ namespace ts { ); } + export function createClassPrivateFieldHelper(context: TransformationContext, privateField: Identifier) { + context.requestEmitHelper({ + name: 'typescript:classPrivateField', + scoped: true, + text: helperString`var ${"_" + privateField.escapedText} = new WeakMap();` + }); + } + + const classPrivateFieldGetHelper: EmitHelper = { + name: "typescript:classPrivateFieldGet", + scoped: false, + 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); };` + } + + export function createClassPrivateFieldGetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier) { + context.requestEmitHelper(classPrivateFieldGetHelper); + return createCall(getHelperName("_classPrivateFieldGet"), /* typeArguments */ undefined, [ receiver, privateField ]); + } + + const classPrivateFieldSetHelper: EmitHelper = { + name: "typescript:classPrivateFieldSet", + scoped: false, + 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; };` + } + + export function createClassPrivateFieldSetHelper(context: TransformationContext, expression: Expression) { + context.requestEmitHelper(classPrivateFieldSetHelper); + return createCall(getHelperName("_classPrivateFieldSet"), /* typeArguments */ undefined, [ expression ]); + } + const awaitHelper: EmitHelper = { name: "typescript:await", scoped: false, From b05e5ad50d28826b5cc25d9d77a2ef9067a42296 Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Thu, 28 Jun 2018 14:33:50 -0400 Subject: [PATCH 04/19] Fix lint errors --- src/compiler/transformers/esnext.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index e21c0dafd93ba..d95ae37dd7de6 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -943,7 +943,7 @@ namespace ts { export function createClassPrivateFieldHelper(context: TransformationContext, privateField: Identifier) { context.requestEmitHelper({ - name: 'typescript:classPrivateField', + name: "typescript:classPrivateField", scoped: true, text: helperString`var ${"_" + privateField.escapedText} = new WeakMap();` }); @@ -953,7 +953,7 @@ namespace ts { name: "typescript:classPrivateFieldGet", scoped: false, 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); };` - } + }; export function createClassPrivateFieldGetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier) { context.requestEmitHelper(classPrivateFieldGetHelper); @@ -964,7 +964,7 @@ namespace ts { name: "typescript:classPrivateFieldSet", scoped: false, 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; };` - } + }; export function createClassPrivateFieldSetHelper(context: TransformationContext, expression: Expression) { context.requestEmitHelper(classPrivateFieldSetHelper); From dcb6ac0230aa2f0b24f1474d0e056e4077819643 Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Fri, 29 Jun 2018 11:43:39 -0400 Subject: [PATCH 05/19] Transform private name references. Generate private field initializers. --- src/compiler/binder.ts | 10 ++ src/compiler/transformers/esnext.ts | 154 +++++++++++++++++++++++++++- 2 files changed, 159 insertions(+), 5 deletions(-) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index b571530c5d9d7..c734a3eb00194 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -3238,6 +3238,11 @@ namespace ts { transformFlags |= TransformFlags.ContainsPropertyInitializer; } + // Private names are an ESNext feature. + if (isIdentifier(node.name) && node.name.isPrivateName) { + transformFlags |= TransformFlags.AssertESNext; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; return transformFlags & ~TransformFlags.NodeExcludes; } @@ -3375,6 +3380,11 @@ namespace ts { transformFlags |= TransformFlags.ContainsSuper; } + // Private names are an ESNext feature. + if (isIdentifier(node.name) && node.name.isPrivateName) { + transformFlags |= TransformFlags.AssertESNext; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; return transformFlags & ~TransformFlags.PropertyAccessExcludes; } diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index d95ae37dd7de6..175bc3577a01b 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -26,6 +26,16 @@ namespace ts { let enclosingFunctionFlags: FunctionFlags; let enclosingSuperContainerFlags: NodeCheckFlags = 0; + + /** + * Maps private names to the generated name of the WeakMap. + */ + interface PrivateNameEnvironment { + [name: string]: { weakMapName: Identifier, initializer: Expression | undefined } + } + let privateNameEnvironmentStack: PrivateNameEnvironment[] = []; + let privateNameEnvironmentIndex = -1; + return chainBundle(transformSourceFile); function transformSourceFile(node: SourceFile) { @@ -105,16 +115,41 @@ namespace ts { return visitPropertyDeclaration(node as PropertyDeclaration); case SyntaxKind.PropertyAccessExpression: return visitPropertyAccessExpression(node as PropertyAccessExpression); + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + return visitClassLikeDeclaration(node as ClassLikeDeclaration); default: return visitEachChild(node, visitor, context); } } + function currentPrivateNameEnvironment() { + return privateNameEnvironmentStack[privateNameEnvironmentIndex]; + } + + function addPrivateNameToEnvironment(name: Identifier, + initializer?: Expression, + environment: PrivateNameEnvironment = currentPrivateNameEnvironment()) { + const nameString = getTextOfIdentifierOrLiteral(name); + if (nameString in environment) { + if (initializer) { + environment[nameString].initializer = initializer; + } + return environment[nameString].weakMapName; + } + const weakMapName = createFileLevelUniqueName('_' + nameString.substring(1)); + environment[nameString] = { + weakMapName, initializer + }; + return weakMapName; + } + function visitPropertyAccessExpression(node: PropertyAccessExpression): Expression { if (node.name.isPrivateName) { + const weakMapName = addPrivateNameToEnvironment(node.name); return setOriginalNode( setTextRange( - createClassPrivateFieldGetHelper(context, node.expression, node.name), + createClassPrivateFieldGetHelper(context, node.expression, weakMapName), /* location */ node ), node @@ -125,11 +160,99 @@ namespace ts { function visitPropertyDeclaration(node: PropertyDeclaration): VisitResult { if (isIdentifier(node.name) && node.name.isPrivateName) { - createClassPrivateFieldHelper(context, node.name); + addPrivateNameToEnvironment(node.name); } return visitEachChild(node, visitor, context); } + function visitClassLikeDeclaration(node: ClassLikeDeclaration): VisitResult { + // Create private name environment. + privateNameEnvironmentStack[++privateNameEnvironmentIndex] = {}; + // Visit children. + node = visitEachChild(node, visitor, context); + // Create WeakMaps for private properties. + // TODO: insert these WeakMap statements somewhere in the code... + const privateNameEnvironment = currentPrivateNameEnvironment(); + for (let propertyName in privateNameEnvironment) { + const { weakMapName } = privateNameEnvironment[propertyName]; + /*const weakMapStatement = */createVariableStatement( + /* modifiers */ undefined, + [ + createVariableDeclaration(weakMapName, + /* typeNode */ undefined, + createNew( + createIdentifier('WeakMap'), + /* typeArguments */ undefined, + /* argumentsArray */ undefined + )) + ] + ); + } + const initializerStatements = Object.keys(privateNameEnvironment).map(name => { + return createStatement( + createCall( + createPropertyAccess(privateNameEnvironment[name].weakMapName, 'set'), + /* typeArguments */ undefined, + [privateNameEnvironment[name].initializer || createVoidZero()] + ) + ); + }); + let members = [...node.members]; + let ctor = find(members, (member) => isConstructorDeclaration(member)); + if (!ctor) { + // Create constructor with private field initializers. + ctor = createConstructor( + /* decorators */ undefined, + /* modifiers */ undefined, + /* parameters */ [], + createBlock(initializerStatements) + ); + members.unshift(ctor); + } else { + // Update existing constructor to add private field initializers. + members = members.map(member => { + if (isConstructorDeclaration(member)) { + let statements = member.body ? + [...initializerStatements, ...member.body.statements] : + initializerStatements; + return updateConstructor( + member, + member.decorators, + member.modifiers, + member.parameters, + createBlock(statements, member.body ? member.body.multiLine : undefined) + ); + } + return member; + }) + } + + // Update class members. + if (isClassDeclaration(node)) { + node = updateClassDeclaration( + node, + node.decorators, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + members + ); + } else if (isClassExpression(node)) { + node = updateClassExpression( + node, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + members + ); + } + // Destroy private name environment. + delete privateNameEnvironmentStack[privateNameEnvironmentIndex--]; + return node; + } + function visitAwaitExpression(node: AwaitExpression): Expression { if (enclosingFunctionFlags & FunctionFlags.Async && enclosingFunctionFlags & FunctionFlags.Generator) { return setOriginalNode( @@ -290,6 +413,18 @@ namespace ts { visitNode(node.right, noDestructuringValue ? visitorNoDestructuringValue : visitor, isExpression) ); } + else if (isAssignmentOperator(node.operatorToken.kind) && + isPropertyAccessExpression(node.left) && + isIdentifier(node.left.name) && + node.left.name.isPrivateName) + { + // If assigning to a private property, rewrite it as a call to the helper function. + const weakMapName = addPrivateNameToEnvironment(node.left.name); + return setOriginalNode( + createClassPrivateFieldSetHelper(context, node.left.expression, weakMapName, node.right), + node + ); + } return visitEachChild(node, visitor, context); } @@ -942,11 +1077,20 @@ namespace ts { } export function createClassPrivateFieldHelper(context: TransformationContext, privateField: Identifier) { + let mapName = null; + const text = (uniqueName: EmitHelperUniqueNameCallback) => { + const str = helperString`var ${"_" + privateField.escapedText} = new WeakMap();`; + return str(name => { + mapName = name; + return uniqueName(name); + }); + }; context.requestEmitHelper({ name: "typescript:classPrivateField", scoped: true, - text: helperString`var ${"_" + privateField.escapedText} = new WeakMap();` + text }); + return mapName; } const classPrivateFieldGetHelper: EmitHelper = { @@ -966,9 +1110,9 @@ namespace ts { 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; };` }; - export function createClassPrivateFieldSetHelper(context: TransformationContext, expression: Expression) { + export function createClassPrivateFieldSetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier, value: Expression) { context.requestEmitHelper(classPrivateFieldSetHelper); - return createCall(getHelperName("_classPrivateFieldSet"), /* typeArguments */ undefined, [ expression ]); + return createCall(getHelperName("_classPrivateFieldSet"), /* typeArguments */ undefined, [ receiver, privateField, value ]); } const awaitHelper: EmitHelper = { From 01d91f1ba2ca7b645dbe028c40cf332f10255472 Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Fri, 29 Jun 2018 12:00:02 -0400 Subject: [PATCH 06/19] Fix private name parser. --- src/compiler/scanner.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index fdd48f1bea7b6..5df5a2a66902e 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -1725,10 +1725,7 @@ namespace ts { return token = SyntaxKind.Unknown; case CharacterCodes.hash: pos++; - if ( - languageVersion === ScriptTarget.ESNext - && isIdentifierStart(ch = text.charCodeAt(pos), languageVersion) - ) { + if (isIdentifierStart(ch = text.charCodeAt(pos), languageVersion)) { tokenFlags |= TokenFlags.PrivateName; pos++; while (pos < end && isIdentifierPart(ch = text.charCodeAt(pos), languageVersion)) pos++; From 3423d1385ee237884e99772c3a87896742d13f6d Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Fri, 29 Jun 2018 12:17:23 -0400 Subject: [PATCH 07/19] Generate WeakMap instances for private fields. Fix field initialization. --- src/compiler/transformers/esnext.ts | 31 +++++++++++++---------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index 175bc3577a01b..e7af7f21b65e2 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -165,35 +165,32 @@ namespace ts { return visitEachChild(node, visitor, context); } - function visitClassLikeDeclaration(node: ClassLikeDeclaration): VisitResult { + function visitClassLikeDeclaration(node: ClassLikeDeclaration): Node[] { // Create private name environment. privateNameEnvironmentStack[++privateNameEnvironmentIndex] = {}; // Visit children. node = visitEachChild(node, visitor, context); // Create WeakMaps for private properties. - // TODO: insert these WeakMap statements somewhere in the code... const privateNameEnvironment = currentPrivateNameEnvironment(); - for (let propertyName in privateNameEnvironment) { - const { weakMapName } = privateNameEnvironment[propertyName]; - /*const weakMapStatement = */createVariableStatement( + const weakMapDeclarations = Object.keys(privateNameEnvironment).map(name => { + const { weakMapName } = privateNameEnvironment[name]; + return createVariableStatement( /* modifiers */ undefined, - [ - createVariableDeclaration(weakMapName, - /* typeNode */ undefined, - createNew( - createIdentifier('WeakMap'), - /* typeArguments */ undefined, - /* argumentsArray */ undefined - )) - ] + [createVariableDeclaration(weakMapName, + /* typeNode */ undefined, + createNew( + createIdentifier('WeakMap'), + /* typeArguments */ undefined, + /* argumentsArray */ undefined + ))] ); - } + }); const initializerStatements = Object.keys(privateNameEnvironment).map(name => { return createStatement( createCall( createPropertyAccess(privateNameEnvironment[name].weakMapName, 'set'), /* typeArguments */ undefined, - [privateNameEnvironment[name].initializer || createVoidZero()] + [createThis(), privateNameEnvironment[name].initializer || createVoidZero()] ) ); }); @@ -250,7 +247,7 @@ namespace ts { } // Destroy private name environment. delete privateNameEnvironmentStack[privateNameEnvironmentIndex--]; - return node; + return [ ...weakMapDeclarations, node ]; } function visitAwaitExpression(node: AwaitExpression): Expression { From 1497ca26462838ee8a12c17c7a9428e620927880 Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Fri, 29 Jun 2018 12:19:11 -0400 Subject: [PATCH 08/19] Remove unused function. --- src/compiler/transformers/esnext.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index e7af7f21b65e2..9792089881024 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -1073,23 +1073,6 @@ namespace ts { ); } - export function createClassPrivateFieldHelper(context: TransformationContext, privateField: Identifier) { - let mapName = null; - const text = (uniqueName: EmitHelperUniqueNameCallback) => { - const str = helperString`var ${"_" + privateField.escapedText} = new WeakMap();`; - return str(name => { - mapName = name; - return uniqueName(name); - }); - }; - context.requestEmitHelper({ - name: "typescript:classPrivateField", - scoped: true, - text - }); - return mapName; - } - const classPrivateFieldGetHelper: EmitHelper = { name: "typescript:classPrivateFieldGet", scoped: false, From 2aa43cddae4c1cb549e3421b4895038a737db729 Mon Sep 17 00:00:00 2001 From: Max Heiber Date: Mon, 2 Jul 2018 20:15:42 -0400 Subject: [PATCH 09/19] remove stray file --- Jenkinsfile | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 4d29d87fd406d..0000000000000 --- a/Jenkinsfile +++ /dev/null @@ -1,25 +0,0 @@ -node('GNRLD') { - stage('checkout') { - deleteDir() - checkout scm - } - - stage('install') { - sh ''' - npm config set "always-auth" "false"; - npm config set "registry" "http://artprod.dev.bloomberg.com:8080/artifactory/api/npm/npm-repos"; - npm config set "strict-ssl" "false"; - npm i --ignore-scripts -s; - npm run build - ''' - } - - stage('lint') { - sh "npm run lint" - } - - stage('test') { - sh "npm run test" - } -} - From 8ce239ff8deca920ba9aaba356bb2f7b1cee8418 Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Mon, 2 Jul 2018 15:35:04 -0400 Subject: [PATCH 10/19] Clean up private name initializer code Signed-off-by: Joseph Watts --- src/compiler/transformers/esnext.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index 9792089881024..3d65c35ed2c4a 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -31,7 +31,7 @@ namespace ts { * Maps private names to the generated name of the WeakMap. */ interface PrivateNameEnvironment { - [name: string]: { weakMapName: Identifier, initializer: Expression | undefined } + [name: string]: Identifier } let privateNameEnvironmentStack: PrivateNameEnvironment[] = []; let privateNameEnvironmentIndex = -1; @@ -128,19 +128,13 @@ namespace ts { } function addPrivateNameToEnvironment(name: Identifier, - initializer?: Expression, environment: PrivateNameEnvironment = currentPrivateNameEnvironment()) { const nameString = getTextOfIdentifierOrLiteral(name); if (nameString in environment) { - if (initializer) { - environment[nameString].initializer = initializer; - } - return environment[nameString].weakMapName; + return environment[nameString]; } const weakMapName = createFileLevelUniqueName('_' + nameString.substring(1)); - environment[nameString] = { - weakMapName, initializer - }; + environment[nameString] = weakMapName; return weakMapName; } @@ -173,7 +167,7 @@ namespace ts { // Create WeakMaps for private properties. const privateNameEnvironment = currentPrivateNameEnvironment(); const weakMapDeclarations = Object.keys(privateNameEnvironment).map(name => { - const { weakMapName } = privateNameEnvironment[name]; + const weakMapName = privateNameEnvironment[name]; return createVariableStatement( /* modifiers */ undefined, [createVariableDeclaration(weakMapName, @@ -188,9 +182,9 @@ namespace ts { const initializerStatements = Object.keys(privateNameEnvironment).map(name => { return createStatement( createCall( - createPropertyAccess(privateNameEnvironment[name].weakMapName, 'set'), + createPropertyAccess(privateNameEnvironment[name], 'set'), /* typeArguments */ undefined, - [createThis(), privateNameEnvironment[name].initializer || createVoidZero()] + [createThis(), createVoidZero()] ) ); }); From dd7c9932e8b207beecc9e0c4160c57301f5a3ec5 Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Tue, 3 Jul 2018 13:36:00 -0400 Subject: [PATCH 11/19] Clean up private field class transformation. Signed-off-by: Joseph Watts --- src/compiler/transformers/esnext.ts | 119 +++++++++++++++------------- 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index 3d65c35ed2c4a..6cb26387dd9e3 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -116,8 +116,9 @@ namespace ts { case SyntaxKind.PropertyAccessExpression: return visitPropertyAccessExpression(node as PropertyAccessExpression); case SyntaxKind.ClassDeclaration: + return visitClassDeclaration(node as ClassDeclaration); case SyntaxKind.ClassExpression: - return visitClassLikeDeclaration(node as ClassLikeDeclaration); + return visitClassExpression(node as ClassExpression); default: return visitEachChild(node, visitor, context); } @@ -159,12 +160,42 @@ namespace ts { return visitEachChild(node, visitor, context); } - function visitClassLikeDeclaration(node: ClassLikeDeclaration): Node[] { + function visitClassDeclaration(node: ClassDeclaration) { + startPrivateNameEnvironment(); + node = visitEachChild(node, visitor, context); + node = updateClassDeclaration( + node, + node.decorators, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + transformClassMembers(node.members) + ); + return [...endPrivateNameEnvironment(), node]; + } + + function visitClassExpression(node: ClassExpression) { + startPrivateNameEnvironment(); + node = visitEachChild(node, visitor, context); + node = updateClassExpression( + node, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + transformClassMembers(node.members) + ); + return [...endPrivateNameEnvironment(), node]; + } + + function startPrivateNameEnvironment() { // Create private name environment. privateNameEnvironmentStack[++privateNameEnvironmentIndex] = {}; - // Visit children. - node = visitEachChild(node, visitor, context); - // Create WeakMaps for private properties. + return currentPrivateNameEnvironment(); + } + + function endPrivateNameEnvironment(): Statement[] { const privateNameEnvironment = currentPrivateNameEnvironment(); const weakMapDeclarations = Object.keys(privateNameEnvironment).map(name => { const weakMapName = privateNameEnvironment[name]; @@ -179,6 +210,15 @@ namespace ts { ))] ); }); + // Destroy private name environment. + delete privateNameEnvironmentStack[privateNameEnvironmentIndex--]; + return weakMapDeclarations; + } + + function transformClassMembers(members: ReadonlyArray): ClassElement[] { + // Rewrite constructor with private name initializers. + const privateNameEnvironment = currentPrivateNameEnvironment(); + // Initialize private properties. const initializerStatements = Object.keys(privateNameEnvironment).map(name => { return createStatement( createCall( @@ -188,60 +228,33 @@ namespace ts { ) ); }); - let members = [...node.members]; - let ctor = find(members, (member) => isConstructorDeclaration(member)); - if (!ctor) { - // Create constructor with private field initializers. - ctor = createConstructor( - /* decorators */ undefined, - /* modifiers */ undefined, - /* parameters */ [], - createBlock(initializerStatements) - ); - members.unshift(ctor); - } else { - // Update existing constructor to add private field initializers. - members = members.map(member => { + const ctor = find(members, (member) => isConstructorDeclaration(member)) as ConstructorDeclaration | undefined; + if (ctor) { + const body = ctor.body ? + updateBlock(ctor.body, [...initializerStatements, ...ctor.body.statements]) : + createBlock(initializerStatements, /* multiLine */ undefined); + return members.map(member => { if (isConstructorDeclaration(member)) { - let statements = member.body ? - [...initializerStatements, ...member.body.statements] : - initializerStatements; return updateConstructor( - member, - member.decorators, - member.modifiers, - member.parameters, - createBlock(statements, member.body ? member.body.multiLine : undefined) + ctor, + ctor.decorators, + ctor.modifiers, + ctor.parameters, + body ); } return member; - }) - } - - // Update class members. - if (isClassDeclaration(node)) { - node = updateClassDeclaration( - node, - node.decorators, - node.modifiers, - node.name, - node.typeParameters, - node.heritageClauses, - members - ); - } else if (isClassExpression(node)) { - node = updateClassExpression( - node, - node.modifiers, - node.name, - node.typeParameters, - node.heritageClauses, - members - ); + }); } - // Destroy private name environment. - delete privateNameEnvironmentStack[privateNameEnvironmentIndex--]; - return [ ...weakMapDeclarations, node ]; + return [ + createConstructor( + /* decorators */ undefined, + /* modifiers */ undefined, + /* parameters */ [], + createBlock(initializerStatements) + ), + ...members + ]; } function visitAwaitExpression(node: AwaitExpression): Expression { From 74986a60c314f26b1452cff537e3793f7aee38c2 Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Tue, 3 Jul 2018 15:00:16 -0400 Subject: [PATCH 12/19] Fix private name transformation clash with constructor overload list Signed-off-by: Joseph Watts --- src/compiler/transformers/esnext.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index 6cb26387dd9e3..e9a0b18070db6 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -228,13 +228,14 @@ namespace ts { ) ); }); - const ctor = find(members, (member) => isConstructorDeclaration(member)) as ConstructorDeclaration | undefined; + const ctor = find( + members, + (member) => isConstructorDeclaration(member) && !!member.body + ) as ConstructorDeclaration | undefined; if (ctor) { - const body = ctor.body ? - updateBlock(ctor.body, [...initializerStatements, ...ctor.body.statements]) : - createBlock(initializerStatements, /* multiLine */ undefined); + const body = updateBlock(ctor.body!, [...initializerStatements, ...ctor.body!.statements]); return members.map(member => { - if (isConstructorDeclaration(member)) { + if (member === ctor) { return updateConstructor( ctor, ctor.decorators, From 0f7a99d236d1b7438b70464f5a638dd8a3019a42 Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Thu, 5 Jul 2018 17:33:26 -0400 Subject: [PATCH 13/19] Remove unnecessary exporting of private name helper functions. Signed-off-by: Joseph Watts --- src/compiler/transformers/esnext.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index e9a0b18070db6..c335b92eeb5fa 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -1087,7 +1087,7 @@ namespace ts { 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); };` }; - export function createClassPrivateFieldGetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier) { + function createClassPrivateFieldGetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier) { context.requestEmitHelper(classPrivateFieldGetHelper); return createCall(getHelperName("_classPrivateFieldGet"), /* typeArguments */ undefined, [ receiver, privateField ]); } @@ -1098,7 +1098,7 @@ namespace ts { 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; };` }; - export function createClassPrivateFieldSetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier, value: Expression) { + function createClassPrivateFieldSetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier, value: Expression) { context.requestEmitHelper(classPrivateFieldSetHelper); return createCall(getHelperName("_classPrivateFieldSet"), /* typeArguments */ undefined, [ receiver, privateField, value ]); } From e0d51d222fb06eb00605a0c200a0219489f0ae5d Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Thu, 19 Jul 2018 16:53:31 -0400 Subject: [PATCH 14/19] Transform compound assignment expressions --- src/compiler/transformers/esnext.ts | 35 +++++++++++++++++++++---- src/compiler/transformers/generators.ts | 22 ---------------- src/compiler/utilities.ts | 22 ++++++++++++++++ 3 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index c335b92eeb5fa..8ea3bec7f267b 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -423,12 +423,37 @@ namespace ts { isIdentifier(node.left.name) && node.left.name.isPrivateName) { - // If assigning to a private property, rewrite it as a call to the helper function. const weakMapName = addPrivateNameToEnvironment(node.left.name); - return setOriginalNode( - createClassPrivateFieldSetHelper(context, node.left.expression, weakMapName, node.right), - node - ); + if (isCompoundAssignment(node.operatorToken.kind)) { + let setReceiver: Expression; + let getReceiver: Identifier; + if (!isIdentifier(node.left.expression) && !isKeyword(node.left.expression.kind)) { + getReceiver = createTempVariable(/* recordTempVariable */ undefined); + hoistVariableDeclaration(getReceiver); + setReceiver = createBinary(getReceiver, SyntaxKind.EqualsToken, node.left.expression); + } else { + getReceiver = node.left.expression as Identifier; + setReceiver = node.left.expression as Identifier; + } + return setOriginalNode( + createClassPrivateFieldSetHelper( + context, + setReceiver, + weakMapName, + createBinary( + createClassPrivateFieldGetHelper(context, getReceiver, weakMapName), + getOperatorForCompoundAssignment(node.operatorToken.kind), + node.right + ) + ), + node + ); + } else { + return setOriginalNode( + createClassPrivateFieldSetHelper(context, node.left.expression, weakMapName, node.right), + node + ); + } } return visitEachChild(node, visitor, context); } diff --git a/src/compiler/transformers/generators.ts b/src/compiler/transformers/generators.ts index a5e10c4b65ddf..9c6f8b875b0ec 100644 --- a/src/compiler/transformers/generators.ts +++ b/src/compiler/transformers/generators.ts @@ -667,28 +667,6 @@ namespace ts { } } - function isCompoundAssignment(kind: BinaryOperator): kind is CompoundAssignmentOperator { - return kind >= SyntaxKind.FirstCompoundAssignment - && kind <= SyntaxKind.LastCompoundAssignment; - } - - function getOperatorForCompoundAssignment(kind: CompoundAssignmentOperator): BitwiseOperatorOrHigher { - switch (kind) { - case SyntaxKind.PlusEqualsToken: return SyntaxKind.PlusToken; - case SyntaxKind.MinusEqualsToken: return SyntaxKind.MinusToken; - case SyntaxKind.AsteriskEqualsToken: return SyntaxKind.AsteriskToken; - case SyntaxKind.AsteriskAsteriskEqualsToken: return SyntaxKind.AsteriskAsteriskToken; - case SyntaxKind.SlashEqualsToken: return SyntaxKind.SlashToken; - case SyntaxKind.PercentEqualsToken: return SyntaxKind.PercentToken; - case SyntaxKind.LessThanLessThanEqualsToken: return SyntaxKind.LessThanLessThanToken; - case SyntaxKind.GreaterThanGreaterThanEqualsToken: return SyntaxKind.GreaterThanGreaterThanToken; - case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: return SyntaxKind.GreaterThanGreaterThanGreaterThanToken; - case SyntaxKind.AmpersandEqualsToken: return SyntaxKind.AmpersandToken; - case SyntaxKind.BarEqualsToken: return SyntaxKind.BarToken; - case SyntaxKind.CaretEqualsToken: return SyntaxKind.CaretToken; - } - } - /** * Visits a right-associative binary expression containing `yield`. * diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index a1c6d5137b27d..41fcc73ed6261 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3697,6 +3697,28 @@ namespace ts { && isLeftHandSideExpression(node.left); } + export function isCompoundAssignment(kind: BinaryOperator): kind is CompoundAssignmentOperator { + return kind >= SyntaxKind.FirstCompoundAssignment + && kind <= SyntaxKind.LastCompoundAssignment; + } + + export function getOperatorForCompoundAssignment(kind: CompoundAssignmentOperator): BitwiseOperatorOrHigher { + switch (kind) { + case SyntaxKind.PlusEqualsToken: return SyntaxKind.PlusToken; + case SyntaxKind.MinusEqualsToken: return SyntaxKind.MinusToken; + case SyntaxKind.AsteriskEqualsToken: return SyntaxKind.AsteriskToken; + case SyntaxKind.AsteriskAsteriskEqualsToken: return SyntaxKind.AsteriskAsteriskToken; + case SyntaxKind.SlashEqualsToken: return SyntaxKind.SlashToken; + case SyntaxKind.PercentEqualsToken: return SyntaxKind.PercentToken; + case SyntaxKind.LessThanLessThanEqualsToken: return SyntaxKind.LessThanLessThanToken; + case SyntaxKind.GreaterThanGreaterThanEqualsToken: return SyntaxKind.GreaterThanGreaterThanToken; + case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: return SyntaxKind.GreaterThanGreaterThanGreaterThanToken; + case SyntaxKind.AmpersandEqualsToken: return SyntaxKind.AmpersandToken; + case SyntaxKind.BarEqualsToken: return SyntaxKind.BarToken; + case SyntaxKind.CaretEqualsToken: return SyntaxKind.CaretToken; + } + } + export function isDestructuringAssignment(node: Node): node is DestructuringAssignment { if (isAssignmentExpression(node, /*excludeCompoundAssignment*/ true)) { const kind = node.left.kind; From 963c24ac44b52973f0a919f3420df7d4e65aae66 Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Thu, 19 Jul 2018 16:58:59 -0400 Subject: [PATCH 15/19] Fix linter errors Signed-off-by: Joseph Watts --- src/compiler/transformers/esnext.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index 8ea3bec7f267b..316261ff4ded5 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -31,9 +31,9 @@ namespace ts { * Maps private names to the generated name of the WeakMap. */ interface PrivateNameEnvironment { - [name: string]: Identifier + [name: string]: Identifier; } - let privateNameEnvironmentStack: PrivateNameEnvironment[] = []; + const privateNameEnvironmentStack: PrivateNameEnvironment[] = []; let privateNameEnvironmentIndex = -1; return chainBundle(transformSourceFile); @@ -134,7 +134,7 @@ namespace ts { if (nameString in environment) { return environment[nameString]; } - const weakMapName = createFileLevelUniqueName('_' + nameString.substring(1)); + const weakMapName = createFileLevelUniqueName("_" + nameString.substring(1)); environment[nameString] = weakMapName; return weakMapName; } @@ -204,7 +204,7 @@ namespace ts { [createVariableDeclaration(weakMapName, /* typeNode */ undefined, createNew( - createIdentifier('WeakMap'), + createIdentifier("WeakMap"), /* typeArguments */ undefined, /* argumentsArray */ undefined ))] @@ -222,7 +222,7 @@ namespace ts { const initializerStatements = Object.keys(privateNameEnvironment).map(name => { return createStatement( createCall( - createPropertyAccess(privateNameEnvironment[name], 'set'), + createPropertyAccess(privateNameEnvironment[name], "set"), /* typeArguments */ undefined, [createThis(), createVoidZero()] ) @@ -421,8 +421,8 @@ namespace ts { else if (isAssignmentOperator(node.operatorToken.kind) && isPropertyAccessExpression(node.left) && isIdentifier(node.left.name) && - node.left.name.isPrivateName) - { + node.left.name.isPrivateName) { + const weakMapName = addPrivateNameToEnvironment(node.left.name); if (isCompoundAssignment(node.operatorToken.kind)) { let setReceiver: Expression; @@ -431,7 +431,8 @@ namespace ts { getReceiver = createTempVariable(/* recordTempVariable */ undefined); hoistVariableDeclaration(getReceiver); setReceiver = createBinary(getReceiver, SyntaxKind.EqualsToken, node.left.expression); - } else { + } + else { getReceiver = node.left.expression as Identifier; setReceiver = node.left.expression as Identifier; } @@ -448,7 +449,8 @@ namespace ts { ), node ); - } else { + } + else { return setOriginalNode( createClassPrivateFieldSetHelper(context, node.left.expression, weakMapName, node.right), node From 3b88811f430c27aa5925468205e2be8d91c1e095 Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Mon, 20 Aug 2018 16:39:35 -0400 Subject: [PATCH 16/19] Use IIFE, transform initializers for classes with private names. --- src/compiler/transformers/es2015.ts | 12 +++++- src/compiler/transformers/esnext.ts | 62 +++++++++++++++++++---------- src/compiler/transformers/ts.ts | 33 ++++++++++----- 3 files changed, 75 insertions(+), 32 deletions(-) diff --git a/src/compiler/transformers/es2015.ts b/src/compiler/transformers/es2015.ts index 8e2b487f13f2a..916addb582d54 100644 --- a/src/compiler/transformers/es2015.ts +++ b/src/compiler/transformers/es2015.ts @@ -3306,11 +3306,14 @@ namespace ts { // The class statements are the statements generated by visiting the first statement with initializer of the // body (1), while all other statements are added to remainingStatements (2) - const isVariableStatementWithInitializer = (stmt: Statement) => isVariableStatement(stmt) && !!first(stmt.declarationList.declarations).initializer; + const isVariableStatementWithInitializer = (stmt: Statement) => !isEndOfDeclarationMarker(stmt) && + isVariableStatement(stmt) && !!first(stmt.declarationList.declarations).initializer; + const isEndOfDeclarationMarker = (stmt: Statement) => stmt.kind === SyntaxKind.EndOfDeclarationMarker; const bodyStatements = visitNodes(body.statements, visitor, isStatement); const classStatements = filter(bodyStatements, isVariableStatementWithInitializer); const remainingStatements = filter(bodyStatements, stmt => !isVariableStatementWithInitializer(stmt)); const varStatement = cast(first(classStatements), isVariableStatement); + const endOfDeclarationMarkers = filter(bodyStatements, isEndOfDeclarationMarker); // We know there is only one variable declaration here as we verified this in an // earlier call to isTypeScriptClassWrapper @@ -3382,12 +3385,17 @@ namespace ts { addRange(statements, funcStatements, classBodyEnd + 1); } + // Add other class statements (such as the WeakMap declarations output by the 'esnext' + // transformer for private names). + addRange(statements, classStatements, /*start*/ 1); + // Add the remaining statements of the outer wrapper. addRange(statements, remainingStatements); // The 'es2015' class transform may add an end-of-declaration marker. If so we will add it // after the remaining statements from the 'ts' transformer. - addRange(statements, classStatements, /*start*/ 1); + addRange(statements, endOfDeclarationMarkers); + // Recreate any outer parentheses or partially-emitted expressions to preserve source map // and comment locations. diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index 316261ff4ded5..290150da464bd 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -31,7 +31,10 @@ namespace ts { * Maps private names to the generated name of the WeakMap. */ interface PrivateNameEnvironment { - [name: string]: Identifier; + [name: string]: { + weakMap: Identifier; + initializer?: Expression; + }; } const privateNameEnvironmentStack: PrivateNameEnvironment[] = []; let privateNameEnvironmentIndex = -1; @@ -111,8 +114,6 @@ namespace ts { return visitParenthesizedExpression(node as ParenthesizedExpression, noDestructuringValue); case SyntaxKind.CatchClause: return visitCatchClause(node as CatchClause); - case SyntaxKind.PropertyDeclaration: - return visitPropertyDeclaration(node as PropertyDeclaration); case SyntaxKind.PropertyAccessExpression: return visitPropertyAccessExpression(node as PropertyAccessExpression); case SyntaxKind.ClassDeclaration: @@ -128,20 +129,32 @@ namespace ts { return privateNameEnvironmentStack[privateNameEnvironmentIndex]; } - function addPrivateNameToEnvironment(name: Identifier, - environment: PrivateNameEnvironment = currentPrivateNameEnvironment()) { + function addPrivateName(name: Identifier, initializer?: Expression) { + const environment = currentPrivateNameEnvironment(); const nameString = getTextOfIdentifierOrLiteral(name); if (nameString in environment) { - return environment[nameString]; + throw new Error("Redeclaring private name " + nameString + "."); } - const weakMapName = createFileLevelUniqueName("_" + nameString.substring(1)); - environment[nameString] = weakMapName; - return weakMapName; + const weakMap = createFileLevelUniqueName("_" + nameString.substring(1)); + environment[nameString] = { + weakMap, + initializer + }; + return weakMap; + } + + function accessPrivateName(name: Identifier) { + const environment = currentPrivateNameEnvironment(); + const nameString = getTextOfIdentifierOrLiteral(name); + if (nameString in environment) { + return environment[nameString].weakMap; + } + throw new Error("Accessing undeclared private name."); } function visitPropertyAccessExpression(node: PropertyAccessExpression): Expression { if (node.name.isPrivateName) { - const weakMapName = addPrivateNameToEnvironment(node.name); + const weakMapName = accessPrivateName(node.name); return setOriginalNode( setTextRange( createClassPrivateFieldGetHelper(context, node.expression, weakMapName), @@ -153,15 +166,21 @@ namespace ts { return visitEachChild(node, visitor, context); } - function visitPropertyDeclaration(node: PropertyDeclaration): VisitResult { - if (isIdentifier(node.name) && node.name.isPrivateName) { - addPrivateNameToEnvironment(node.name); + function visitorCollectPrivateNames(node: Node): VisitResult { + if (isPropertyDeclaration(node) && isIdentifier(node.name) && node.name.isPrivateName) { + addPrivateName(node.name, node.initializer); + return undefined; } - return visitEachChild(node, visitor, context); + // Don't collect private names from nested classes. + if (isClassLike(node)) { + return node; + } + return visitEachChild(node, visitorCollectPrivateNames, context); } function visitClassDeclaration(node: ClassDeclaration) { startPrivateNameEnvironment(); + node = visitEachChild(node, visitorCollectPrivateNames, context); node = visitEachChild(node, visitor, context); node = updateClassDeclaration( node, @@ -172,7 +191,7 @@ namespace ts { node.heritageClauses, transformClassMembers(node.members) ); - return [...endPrivateNameEnvironment(), node]; + return [node, ...endPrivateNameEnvironment()]; } function visitClassExpression(node: ClassExpression) { @@ -186,7 +205,7 @@ namespace ts { node.heritageClauses, transformClassMembers(node.members) ); - return [...endPrivateNameEnvironment(), node]; + return [node, ...endPrivateNameEnvironment()]; } function startPrivateNameEnvironment() { @@ -198,10 +217,10 @@ namespace ts { function endPrivateNameEnvironment(): Statement[] { const privateNameEnvironment = currentPrivateNameEnvironment(); const weakMapDeclarations = Object.keys(privateNameEnvironment).map(name => { - const weakMapName = privateNameEnvironment[name]; + const privateName = privateNameEnvironment[name]; return createVariableStatement( /* modifiers */ undefined, - [createVariableDeclaration(weakMapName, + [createVariableDeclaration(privateName.weakMap, /* typeNode */ undefined, createNew( createIdentifier("WeakMap"), @@ -220,11 +239,12 @@ namespace ts { const privateNameEnvironment = currentPrivateNameEnvironment(); // Initialize private properties. const initializerStatements = Object.keys(privateNameEnvironment).map(name => { + const privateName = privateNameEnvironment[name]; return createStatement( createCall( - createPropertyAccess(privateNameEnvironment[name], "set"), + createPropertyAccess(privateName.weakMap, "set"), /* typeArguments */ undefined, - [createThis(), createVoidZero()] + [createThis(), privateName.initializer || createVoidZero()] ) ); }); @@ -423,7 +443,7 @@ namespace ts { isIdentifier(node.left.name) && node.left.name.isPrivateName) { - const weakMapName = addPrivateNameToEnvironment(node.left.name); + const weakMapName = accessPrivateName(node.left.name); if (isCompoundAssignment(node.operatorToken.kind)) { let setReceiver: Expression; let getReceiver: Identifier; diff --git a/src/compiler/transformers/ts.ts b/src/compiler/transformers/ts.ts index a0d941f1deffe..b75453fb95ab9 100644 --- a/src/compiler/transformers/ts.ts +++ b/src/compiler/transformers/ts.ts @@ -593,9 +593,10 @@ namespace ts { return parameter.decorators !== undefined && parameter.decorators.length > 0; } - function getClassFacts(node: ClassDeclaration, staticProperties: ReadonlyArray) { + function getClassFacts(node: ClassDeclaration, staticProperties: ReadonlyArray, privateProperties: ReadonlyArray) { let facts = ClassFacts.None; if (some(staticProperties)) facts |= ClassFacts.HasStaticInitializedProperties; + if (languageVersion < ScriptTarget.ESNext && some(privateProperties)) facts |= ClassFacts.UseImmediatelyInvokedFunctionExpression; const extendsClauseElement = getEffectiveBaseTypeNode(node); if (extendsClauseElement && skipOuterExpressions(extendsClauseElement.expression).kind !== SyntaxKind.NullKeyword) facts |= ClassFacts.IsDerivedClass; if (shouldEmitDecorateCallForClass(node)) facts |= ClassFacts.HasConstructorDecorators; @@ -623,7 +624,7 @@ namespace ts { pendingExpressions = undefined; const staticProperties = getInitializedProperties(node, /*isStatic*/ true); - const facts = getClassFacts(node, staticProperties); + const facts = getClassFacts(node, staticProperties, getPrivateProperties(node)); if (facts & ClassFacts.UseImmediatelyInvokedFunctionExpression) { context.startLexicalEnvironment(); @@ -973,13 +974,13 @@ namespace ts { // Check if we have property assignment inside class declaration. // If there is a property assignment, we need to emit constructor whether users define it or not // If there is no property assignment, we can omit constructor if users do not define it - const hasInstancePropertyWithInitializer = forEach(node.members, isInstanceInitializedProperty); + const hasNonPrivateInstancePropertyWithInitializer = forEach(node.members, member => isInstanceInitializedProperty(member) && isPrivateProperty(member)); const hasParameterPropertyAssignments = node.transformFlags & TransformFlags.ContainsParameterPropertyAssignments; const constructor = getFirstConstructorWithBody(node); // If the class does not contain nodes that require a synthesized constructor, // accept the current constructor if it exists. - if (!hasInstancePropertyWithInitializer && !hasParameterPropertyAssignments) { + if (!hasNonPrivateInstancePropertyWithInitializer && !hasParameterPropertyAssignments) { return visitEachChild(constructor, visitor, context); } @@ -1194,6 +1195,14 @@ namespace ts { ); } + function isPrivateProperty(member: ClassElement): member is PropertyDeclaration { + return member.kind === SyntaxKind.PropertyDeclaration && + !!member.name && isIdentifier(member.name) && member.name.isPrivateName; + } + + function getPrivateProperties(node: ClassExpression | ClassDeclaration): ReadonlyArray { + return filter(node.members, isPrivateProperty); + } /** * Gets all property declarations with initializers on either the static or instance side of a class. * @@ -1242,10 +1251,12 @@ namespace ts { */ function addInitializedPropertyStatements(statements: Statement[], properties: ReadonlyArray, receiver: LeftHandSideExpression) { for (const property of properties) { - const statement = createStatement(transformInitializedProperty(property, receiver)); - setSourceMapRange(statement, moveRangePastModifiers(property)); - setCommentRange(statement, property); - statements.push(statement); + if (!isPrivateProperty(property)) { + const statement = createStatement(transformInitializedProperty(property, receiver)); + setSourceMapRange(statement, moveRangePastModifiers(property)); + setCommentRange(statement, property); + statements.push(statement); + } } } @@ -2226,7 +2237,11 @@ namespace ts { return !nodeIsMissing(node.body); } - function visitPropertyDeclaration(node: PropertyDeclaration): undefined { + function visitPropertyDeclaration(node: PropertyDeclaration): PropertyDeclaration | undefined { + if (isIdentifier(node.name) && node.name.isPrivateName) { + // Keep the private name declaration. + return node; + } const expr = getPropertyNameExpressionIfNeeded(node.name, some(node.decorators) || !!node.initializer, /*omitSimple*/ true); if (expr && !isSimpleInlineableExpression(expr)) { (pendingExpressions || (pendingExpressions = [])).push(expr); From b7524964483f9b3b330af72caf8a1c485a9490e7 Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Tue, 21 Aug 2018 14:34:50 -0400 Subject: [PATCH 17/19] Fix class expression transformation, punt on IIFE class wrappers --- src/compiler/transformers/esnext.ts | 45 ++++++++++++++++++++++------- src/compiler/transformers/ts.ts | 26 ++++++++--------- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index 290150da464bd..403e397ae1aa8 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -191,7 +191,13 @@ namespace ts { node.heritageClauses, transformClassMembers(node.members) ); - return [node, ...endPrivateNameEnvironment()]; + const statements = [ + node, + ...createPrivateNameWeakMapDeclarations( + endPrivateNameEnvironment() + ) + ]; + return statements; } function visitClassExpression(node: ClassExpression) { @@ -205,7 +211,9 @@ namespace ts { node.heritageClauses, transformClassMembers(node.members) ); - return [node, ...endPrivateNameEnvironment()]; + const expressions = createPrivateNameWeakMapAssignments(endPrivateNameEnvironment()); + expressions.push(node); + return createCommaList(expressions); } function startPrivateNameEnvironment() { @@ -214,24 +222,39 @@ namespace ts { return currentPrivateNameEnvironment(); } - function endPrivateNameEnvironment(): Statement[] { + function endPrivateNameEnvironment(): PrivateNameEnvironment { const privateNameEnvironment = currentPrivateNameEnvironment(); - const weakMapDeclarations = Object.keys(privateNameEnvironment).map(name => { - const privateName = privateNameEnvironment[name]; + // Destroy private name environment. + delete privateNameEnvironmentStack[privateNameEnvironmentIndex--]; + return privateNameEnvironment; + } + + function createPrivateNameWeakMapDeclarations(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"), + createNew( + createIdentifier("WeakMap"), /* typeArguments */ undefined, /* argumentsArray */ undefined - ))] + ))] + ); + }); + } + + function createPrivateNameWeakMapAssignments(environment: PrivateNameEnvironment): Expression[] { + return Object.keys(environment).map(name => { + const privateName = environment[name]; + hoistVariableDeclaration(privateName.weakMap); + return createBinary( + privateName.weakMap, + SyntaxKind.EqualsToken, + createNew(createIdentifier("WeakMap"), /* typeArguments */ undefined, /* argumentsArray */ undefined) ); }); - // Destroy private name environment. - delete privateNameEnvironmentStack[privateNameEnvironmentIndex--]; - return weakMapDeclarations; } function transformClassMembers(members: ReadonlyArray): ClassElement[] { diff --git a/src/compiler/transformers/ts.ts b/src/compiler/transformers/ts.ts index b75453fb95ab9..43b890edb2241 100644 --- a/src/compiler/transformers/ts.ts +++ b/src/compiler/transformers/ts.ts @@ -593,10 +593,9 @@ namespace ts { return parameter.decorators !== undefined && parameter.decorators.length > 0; } - function getClassFacts(node: ClassDeclaration, staticProperties: ReadonlyArray, privateProperties: ReadonlyArray) { + function getClassFacts(node: ClassDeclaration, staticProperties: ReadonlyArray) { let facts = ClassFacts.None; if (some(staticProperties)) facts |= ClassFacts.HasStaticInitializedProperties; - if (languageVersion < ScriptTarget.ESNext && some(privateProperties)) facts |= ClassFacts.UseImmediatelyInvokedFunctionExpression; const extendsClauseElement = getEffectiveBaseTypeNode(node); if (extendsClauseElement && skipOuterExpressions(extendsClauseElement.expression).kind !== SyntaxKind.NullKeyword) facts |= ClassFacts.IsDerivedClass; if (shouldEmitDecorateCallForClass(node)) facts |= ClassFacts.HasConstructorDecorators; @@ -624,7 +623,7 @@ namespace ts { pendingExpressions = undefined; const staticProperties = getInitializedProperties(node, /*isStatic*/ true); - const facts = getClassFacts(node, staticProperties, getPrivateProperties(node)); + const facts = getClassFacts(node, staticProperties); if (facts & ClassFacts.UseImmediatelyInvokedFunctionExpression) { context.startLexicalEnvironment(); @@ -974,13 +973,13 @@ namespace ts { // Check if we have property assignment inside class declaration. // If there is a property assignment, we need to emit constructor whether users define it or not // If there is no property assignment, we can omit constructor if users do not define it - const hasNonPrivateInstancePropertyWithInitializer = forEach(node.members, member => isInstanceInitializedProperty(member) && isPrivateProperty(member)); + const hasInstancePropertyWithInitializer = forEach(node.members, member => isInstanceInitializedProperty(member) && !isPrivateProperty(member)); const hasParameterPropertyAssignments = node.transformFlags & TransformFlags.ContainsParameterPropertyAssignments; const constructor = getFirstConstructorWithBody(node); // If the class does not contain nodes that require a synthesized constructor, // accept the current constructor if it exists. - if (!hasNonPrivateInstancePropertyWithInitializer && !hasParameterPropertyAssignments) { + if (!hasInstancePropertyWithInitializer && !hasParameterPropertyAssignments) { return visitEachChild(constructor, visitor, context); } @@ -1200,9 +1199,6 @@ namespace ts { !!member.name && isIdentifier(member.name) && member.name.isPrivateName; } - function getPrivateProperties(node: ClassExpression | ClassDeclaration): ReadonlyArray { - return filter(node.members, isPrivateProperty); - } /** * Gets all property declarations with initializers on either the static or instance side of a class. * @@ -1251,12 +1247,13 @@ namespace ts { */ function addInitializedPropertyStatements(statements: Statement[], properties: ReadonlyArray, receiver: LeftHandSideExpression) { for (const property of properties) { - if (!isPrivateProperty(property)) { - const statement = createStatement(transformInitializedProperty(property, receiver)); - setSourceMapRange(statement, moveRangePastModifiers(property)); - setCommentRange(statement, property); - statements.push(statement); + if (isPrivateProperty(property)) { + continue; } + const statement = createStatement(transformInitializedProperty(property, receiver)); + setSourceMapRange(statement, moveRangePastModifiers(property)); + setCommentRange(statement, property); + statements.push(statement); } } @@ -1269,6 +1266,9 @@ namespace ts { function generateInitializedPropertyExpressions(properties: ReadonlyArray, receiver: LeftHandSideExpression) { const expressions: Expression[] = []; for (const property of properties) { + if (isPrivateProperty(property)) { + continue; + } const expression = transformInitializedProperty(property, receiver); startOnNewLine(expression); setSourceMapRange(expression, moveRangePastModifiers(property)); From 226763d549e477851d890906b63ca2a7c4011076 Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Tue, 21 Aug 2018 15:22:06 -0400 Subject: [PATCH 18/19] Only transform classes when there are private names. --- src/compiler/transformers/esnext.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index 403e397ae1aa8..1aee44e658668 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -182,6 +182,9 @@ namespace ts { startPrivateNameEnvironment(); node = visitEachChild(node, visitorCollectPrivateNames, context); node = visitEachChild(node, visitor, context); + if (!currentPrivateNameEnvironment().length) { + return node; + } node = updateClassDeclaration( node, node.decorators, @@ -203,6 +206,9 @@ namespace ts { function visitClassExpression(node: ClassExpression) { startPrivateNameEnvironment(); node = visitEachChild(node, visitor, context); + if (!currentPrivateNameEnvironment().length) { + return node; + } node = updateClassExpression( node, node.modifiers, From 6e037edfedd069ea27be5ccc4a26478839cb25fc Mon Sep 17 00:00:00 2001 From: Joseph Watts Date: Tue, 21 Aug 2018 16:00:16 -0400 Subject: [PATCH 19/19] Fix weak map generation and class transformation --- src/compiler/transformers/esnext.ts | 59 +++++++++++++++-------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index 1aee44e658668..217faad7be2ad 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -182,44 +182,45 @@ namespace ts { startPrivateNameEnvironment(); node = visitEachChild(node, visitorCollectPrivateNames, context); node = visitEachChild(node, visitor, context); - if (!currentPrivateNameEnvironment().length) { - return node; - } - node = updateClassDeclaration( - node, - node.decorators, - node.modifiers, - node.name, - node.typeParameters, - node.heritageClauses, - transformClassMembers(node.members) + const statements = createPrivateNameWeakMapDeclarations( + currentPrivateNameEnvironment() ); - const statements = [ - node, - ...createPrivateNameWeakMapDeclarations( - endPrivateNameEnvironment() - ) - ]; + if (statements.length) { + node = updateClassDeclaration( + node, + node.decorators, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + transformClassMembers(node.members) + ); + } + prependStatements(statements, [node]); + endPrivateNameEnvironment(); return statements; } function visitClassExpression(node: ClassExpression) { startPrivateNameEnvironment(); + node = visitEachChild(node, visitorCollectPrivateNames, context); node = visitEachChild(node, visitor, context); - if (!currentPrivateNameEnvironment().length) { - return node; - } - node = updateClassExpression( - node, - node.modifiers, - node.name, - node.typeParameters, - node.heritageClauses, - transformClassMembers(node.members) + const expressions = createPrivateNameWeakMapAssignments( + currentPrivateNameEnvironment() ); - const expressions = createPrivateNameWeakMapAssignments(endPrivateNameEnvironment()); + if (expressions.length) { + node = updateClassExpression( + node, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + transformClassMembers(node.members) + ); + } expressions.push(node); - return createCommaList(expressions); + endPrivateNameEnvironment(); + return expressions.length > 1 ? createCommaList(expressions) : expressions[0]; } function startPrivateNameEnvironment() {