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/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++; 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 cf1c6c447bee9..217faad7be2ad 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -26,6 +26,19 @@ namespace ts { let enclosingFunctionFlags: FunctionFlags; let enclosingSuperContainerFlags: NodeCheckFlags = 0; + + /** + * Maps private names to the generated name of the WeakMap. + */ + interface PrivateNameEnvironment { + [name: string]: { + weakMap: Identifier; + initializer?: Expression; + }; + } + const privateNameEnvironmentStack: PrivateNameEnvironment[] = []; + let privateNameEnvironmentIndex = -1; + return chainBundle(transformSourceFile); function transformSourceFile(node: SourceFile) { @@ -101,11 +114,200 @@ namespace ts { return visitParenthesizedExpression(node as ParenthesizedExpression, noDestructuringValue); case SyntaxKind.CatchClause: return visitCatchClause(node as CatchClause); + case SyntaxKind.PropertyAccessExpression: + return visitPropertyAccessExpression(node as PropertyAccessExpression); + case SyntaxKind.ClassDeclaration: + return visitClassDeclaration(node as ClassDeclaration); + case SyntaxKind.ClassExpression: + return visitClassExpression(node as ClassExpression); default: return visitEachChild(node, visitor, context); } } + function currentPrivateNameEnvironment() { + return privateNameEnvironmentStack[privateNameEnvironmentIndex]; + } + + function addPrivateName(name: Identifier, initializer?: Expression) { + const environment = currentPrivateNameEnvironment(); + const nameString = getTextOfIdentifierOrLiteral(name); + if (nameString in environment) { + throw new Error("Redeclaring private name " + nameString + "."); + } + 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 = accessPrivateName(node.name); + return setOriginalNode( + setTextRange( + createClassPrivateFieldGetHelper(context, node.expression, weakMapName), + /* location */ node + ), + node + ); + } + return visitEachChild(node, visitor, context); + } + + function visitorCollectPrivateNames(node: Node): VisitResult { + if (isPropertyDeclaration(node) && isIdentifier(node.name) && node.name.isPrivateName) { + addPrivateName(node.name, node.initializer); + return undefined; + } + // 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); + const statements = createPrivateNameWeakMapDeclarations( + currentPrivateNameEnvironment() + ); + 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); + const expressions = createPrivateNameWeakMapAssignments( + currentPrivateNameEnvironment() + ); + if (expressions.length) { + node = updateClassExpression( + node, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + transformClassMembers(node.members) + ); + } + expressions.push(node); + endPrivateNameEnvironment(); + return expressions.length > 1 ? createCommaList(expressions) : expressions[0]; + } + + function startPrivateNameEnvironment() { + // Create private name environment. + privateNameEnvironmentStack[++privateNameEnvironmentIndex] = {}; + return currentPrivateNameEnvironment(); + } + + function endPrivateNameEnvironment(): PrivateNameEnvironment { + const privateNameEnvironment = currentPrivateNameEnvironment(); + // 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"), + /* 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) + ); + }); + } + + function transformClassMembers(members: ReadonlyArray): ClassElement[] { + // Rewrite constructor with private name initializers. + 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 ctor = find( + members, + (member) => isConstructorDeclaration(member) && !!member.body + ) as ConstructorDeclaration | undefined; + if (ctor) { + const body = updateBlock(ctor.body!, [...initializerStatements, ...ctor.body!.statements]); + return members.map(member => { + if (member === ctor) { + return updateConstructor( + ctor, + ctor.decorators, + ctor.modifiers, + ctor.parameters, + body + ); + } + return member; + }); + } + return [ + createConstructor( + /* decorators */ undefined, + /* modifiers */ undefined, + /* parameters */ [], + createBlock(initializerStatements) + ), + ...members + ]; + } + function visitAwaitExpression(node: AwaitExpression): Expression { if (enclosingFunctionFlags & FunctionFlags.Async && enclosingFunctionFlags & FunctionFlags.Generator) { return setOriginalNode( @@ -266,6 +468,45 @@ 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) { + + const weakMapName = accessPrivateName(node.left.name); + 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); } @@ -917,6 +1158,28 @@ namespace ts { ); } + 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); };` + }; + + 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; };` + }; + + function createClassPrivateFieldSetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier, value: Expression) { + context.requestEmitHelper(classPrivateFieldSetHelper); + return createCall(getHelperName("_classPrivateFieldSet"), /* typeArguments */ undefined, [ receiver, privateField, value ]); + } + const awaitHelper: EmitHelper = { name: "typescript:await", scoped: false, 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/transformers/ts.ts b/src/compiler/transformers/ts.ts index a0d941f1deffe..43b890edb2241 100644 --- a/src/compiler/transformers/ts.ts +++ b/src/compiler/transformers/ts.ts @@ -973,7 +973,7 @@ 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 hasInstancePropertyWithInitializer = forEach(node.members, member => isInstanceInitializedProperty(member) && !isPrivateProperty(member)); const hasParameterPropertyAssignments = node.transformFlags & TransformFlags.ContainsParameterPropertyAssignments; const constructor = getFirstConstructorWithBody(node); @@ -1194,6 +1194,11 @@ namespace ts { ); } + function isPrivateProperty(member: ClassElement): member is PropertyDeclaration { + return member.kind === SyntaxKind.PropertyDeclaration && + !!member.name && isIdentifier(member.name) && member.name.isPrivateName; + } + /** * Gets all property declarations with initializers on either the static or instance side of a class. * @@ -1242,6 +1247,9 @@ namespace ts { */ function addInitializedPropertyStatements(statements: Statement[], properties: ReadonlyArray, receiver: LeftHandSideExpression) { for (const property of properties) { + if (isPrivateProperty(property)) { + continue; + } const statement = createStatement(transformInitializedProperty(property, receiver)); setSourceMapRange(statement, moveRangePastModifiers(property)); setCommentRange(statement, property); @@ -1258,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)); @@ -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); 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;