diff --git a/src/compiler/transformers/esnext.ts b/src/compiler/transformers/esnext.ts index fa1c3384be02d..90124cef3c96f 100644 --- a/src/compiler/transformers/esnext.ts +++ b/src/compiler/transformers/esnext.ts @@ -758,20 +758,105 @@ namespace ts { return visitEachChild(node, visitor, context); } + function wrapPrivateNameForDestructuringTarget(node: PrivateNamedPropertyAccess) { + return createPropertyAccess( + createObjectLiteral([ + createSetAccessor( + /*decorators*/ undefined, + /*modifiers*/ undefined, + "value", + [createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, "x", + /*questionToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined + )], + createBlock( + [createExpressionStatement( + createAssignment( + visitNode(node, visitor), + createIdentifier("x") + ) + )] + ) + ) + ]), + "value" + ); + } + + function transformDestructuringAssignmentTarget(node: ArrayLiteralExpression | ObjectLiteralExpression) { + const hasPrivateNames = isArrayLiteralExpression(node) ? + forEach(node.elements, isPrivateNamedPropertyAccess) : + forEach(node.properties, property => isPropertyAssignment(property) && isPrivateNamedPropertyAccess(property.initializer)); + if (!hasPrivateNames) { + return node; + } + if (isArrayLiteralExpression(node)) { + // Transforms private names in destructuring assignment array bindings. + // + // Source: + // ([ this.#myProp ] = [ "hello" ]); + // + // Transformation: + // [ { set value(x) { this.#myProp = x; } }.value ] = [ "hello" ]; + return updateArrayLiteral( + node, + node.elements.map( + expr => isPrivateNamedPropertyAccess(expr) ? + wrapPrivateNameForDestructuringTarget(expr) : + expr + ) + ); + } + else { + // Transforms private names in destructuring assignment object bindings. + // + // Source: + // ({ stringProperty: this.#myProp } = { stringProperty: "hello" }); + // + // Transformation: + // ({ stringProperty: { set value(x) { this.#myProp = x; } }.value }) = { stringProperty: "hello" }; + return updateObjectLiteral( + node, + node.properties.map( + prop => isPropertyAssignment(prop) && isPrivateNamedPropertyAccess(prop.initializer) ? + updatePropertyAssignment( + prop, + prop.name, + wrapPrivateNameForDestructuringTarget(prop.initializer) + ) : + prop + ) + ); + } + } + /** * Visits a BinaryExpression that contains a destructuring assignment. * * @param node A BinaryExpression node. */ function visitBinaryExpression(node: BinaryExpression, noDestructuringValue: boolean): Expression { - if (isDestructuringAssignment(node) && node.left.transformFlags & TransformFlags.ContainsObjectRestOrSpread) { - return flattenDestructuringAssignment( - node, - visitor, - context, - FlattenLevel.ObjectRest, - !noDestructuringValue - ); + if (isDestructuringAssignment(node)) { + const left = transformDestructuringAssignmentTarget(node.left); + if (left !== node.left || node.left.transformFlags & TransformFlags.ContainsObjectRestOrSpread) { + return flattenDestructuringAssignment( + left === node.left ? node : updateBinary( + node, + left, + node.right, + node.operatorToken.kind + ) as DestructuringAssignment, + visitor, + context, + node.left.transformFlags & TransformFlags.ContainsObjectRestOrSpread + ? FlattenLevel.ObjectRest : FlattenLevel.All, + !noDestructuringValue + ); + } } else if (node.operatorToken.kind === SyntaxKind.CommaToken) { return updateBinary( diff --git a/src/compiler/types.ts b/src/compiler/types.ts index bd0e5a8adedef..202ee73e874c7 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1756,6 +1756,10 @@ namespace ts { name: Identifier | PrivateName; } + export interface PrivateNamedPropertyAccess extends PropertyAccessExpression { + name: PrivateName; + } + export interface SuperPropertyAccessExpression extends PropertyAccessExpression { expression: SuperExpression; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 475b971f4c32d..41d12fc64e639 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -6076,6 +6076,10 @@ namespace ts { return isPropertyDeclaration(node) && isPrivateName(node.name); } + export function isPrivateNamedPropertyAccess(node: Node): node is PrivateNamedPropertyAccess { + return isPropertyAccessExpression(node) && isPrivateName(node.name); + } + export function isBindingName(node: Node): node is BindingName { const kind = node.kind; return kind === SyntaxKind.Identifier diff --git a/tests/baselines/reference/privateNameFieldDestructuredBinding.js b/tests/baselines/reference/privateNameFieldDestructuredBinding.js new file mode 100644 index 0000000000000..e76d7978b8d4a --- /dev/null +++ b/tests/baselines/reference/privateNameFieldDestructuredBinding.js @@ -0,0 +1,37 @@ +//// [privateNameFieldDestructuredBinding.ts] +class A { + #field = 1; + testObject() { + return { x: 10, y: 6 }; + } + testArray() { + return [10, 11]; + } + constructor() { + let y: number; + ({ x: this.#field, y } = this.testObject()); + ([this.#field, y] = this.testArray()); + } +} + + +//// [privateNameFieldDestructuredBinding.js] +var _classPrivateFieldGet = function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return privateMap.get(receiver); }; +var _field; +var A = /** @class */ (function () { + function A() { + _field.set(this, 1); + var _a, _b; + var y; + (_a = this.testObject(), { set value(x) { _classPrivateFieldGet(this, _field) = x; } }.value = _a.x, y = _a.y); + (_b = this.testArray(), { set value(x) { _classPrivateFieldGet(this, _field) = x; } }.value = _b[0], y = _b[1]); + } + A.prototype.testObject = function () { + return { x: 10, y: 6 }; + }; + A.prototype.testArray = function () { + return [10, 11]; + }; + return A; +}()); +_field = new WeakMap(); diff --git a/tests/baselines/reference/privateNameFieldDestructuredBinding.symbols b/tests/baselines/reference/privateNameFieldDestructuredBinding.symbols new file mode 100644 index 0000000000000..ced97920fca56 --- /dev/null +++ b/tests/baselines/reference/privateNameFieldDestructuredBinding.symbols @@ -0,0 +1,42 @@ +=== tests/cases/conformance/classes/members/privateNames/privateNameFieldDestructuredBinding.ts === +class A { +>A : Symbol(A, Decl(privateNameFieldDestructuredBinding.ts, 0, 0)) + + #field = 1; +>#field : Symbol(A[#field], Decl(privateNameFieldDestructuredBinding.ts, 0, 9)) + + testObject() { +>testObject : Symbol(A.testObject, Decl(privateNameFieldDestructuredBinding.ts, 1, 15)) + + return { x: 10, y: 6 }; +>x : Symbol(x, Decl(privateNameFieldDestructuredBinding.ts, 3, 16)) +>y : Symbol(y, Decl(privateNameFieldDestructuredBinding.ts, 3, 23)) + } + testArray() { +>testArray : Symbol(A.testArray, Decl(privateNameFieldDestructuredBinding.ts, 4, 5)) + + return [10, 11]; + } + constructor() { + let y: number; +>y : Symbol(y, Decl(privateNameFieldDestructuredBinding.ts, 9, 11)) + + ({ x: this.#field, y } = this.testObject()); +>x : Symbol(x, Decl(privateNameFieldDestructuredBinding.ts, 10, 10)) +>this.#field : Symbol(A[#field], Decl(privateNameFieldDestructuredBinding.ts, 0, 9)) +>this : Symbol(A, Decl(privateNameFieldDestructuredBinding.ts, 0, 0)) +>y : Symbol(y, Decl(privateNameFieldDestructuredBinding.ts, 10, 26)) +>this.testObject : Symbol(A.testObject, Decl(privateNameFieldDestructuredBinding.ts, 1, 15)) +>this : Symbol(A, Decl(privateNameFieldDestructuredBinding.ts, 0, 0)) +>testObject : Symbol(A.testObject, Decl(privateNameFieldDestructuredBinding.ts, 1, 15)) + + ([this.#field, y] = this.testArray()); +>this.#field : Symbol(A[#field], Decl(privateNameFieldDestructuredBinding.ts, 0, 9)) +>this : Symbol(A, Decl(privateNameFieldDestructuredBinding.ts, 0, 0)) +>y : Symbol(y, Decl(privateNameFieldDestructuredBinding.ts, 9, 11)) +>this.testArray : Symbol(A.testArray, Decl(privateNameFieldDestructuredBinding.ts, 4, 5)) +>this : Symbol(A, Decl(privateNameFieldDestructuredBinding.ts, 0, 0)) +>testArray : Symbol(A.testArray, Decl(privateNameFieldDestructuredBinding.ts, 4, 5)) + } +} + diff --git a/tests/baselines/reference/privateNameFieldDestructuredBinding.types b/tests/baselines/reference/privateNameFieldDestructuredBinding.types new file mode 100644 index 0000000000000..b948b04e66cd7 --- /dev/null +++ b/tests/baselines/reference/privateNameFieldDestructuredBinding.types @@ -0,0 +1,57 @@ +=== tests/cases/conformance/classes/members/privateNames/privateNameFieldDestructuredBinding.ts === +class A { +>A : A + + #field = 1; +>#field : number +>1 : 1 + + testObject() { +>testObject : () => { x: number; y: number; } + + return { x: 10, y: 6 }; +>{ x: 10, y: 6 } : { x: number; y: number; } +>x : number +>10 : 10 +>y : number +>6 : 6 + } + testArray() { +>testArray : () => number[] + + return [10, 11]; +>[10, 11] : number[] +>10 : 10 +>11 : 11 + } + constructor() { + let y: number; +>y : number + + ({ x: this.#field, y } = this.testObject()); +>({ x: this.#field, y } = this.testObject()) : { x: number; y: number; } +>{ x: this.#field, y } = this.testObject() : { x: number; y: number; } +>{ x: this.#field, y } : { x: number; y: number; } +>x : number +>this.#field : number +>this : this +>y : number +>this.testObject() : { x: number; y: number; } +>this.testObject : () => { x: number; y: number; } +>this : this +>testObject : () => { x: number; y: number; } + + ([this.#field, y] = this.testArray()); +>([this.#field, y] = this.testArray()) : number[] +>[this.#field, y] = this.testArray() : number[] +>[this.#field, y] : [number, number] +>this.#field : number +>this : this +>y : number +>this.testArray() : number[] +>this.testArray : () => number[] +>this : this +>testArray : () => number[] + } +} + diff --git a/tests/cases/conformance/classes/members/privateNames/privateNameFieldDestructuredBinding.ts b/tests/cases/conformance/classes/members/privateNames/privateNameFieldDestructuredBinding.ts new file mode 100644 index 0000000000000..4c746be2cd67a --- /dev/null +++ b/tests/cases/conformance/classes/members/privateNames/privateNameFieldDestructuredBinding.ts @@ -0,0 +1,14 @@ +class A { + #field = 1; + testObject() { + return { x: 10, y: 6 }; + } + testArray() { + return [10, 11]; + } + constructor() { + let y: number; + ({ x: this.#field, y } = this.testObject()); + ([this.#field, y] = this.testArray()); + } +}