From 0859535b620ddbc0a835c7882efbf3b5660df3b0 Mon Sep 17 00:00:00 2001 From: Tim McClure Date: Wed, 28 Nov 2018 19:20:09 -0500 Subject: [PATCH] Private class methods stage 3 (#8654) * Add private method syntax support * Add private method spec support * Add private method loose support * Throw error if static private method is used * Add more isStatic & isMethod checks * Remove `writable:false` from private method inits `writable` is false by default. * Add private method func obj equality check * Throw if private accessor is used * Add check for fields === private method loose mode * Throw buildCodeFrameErrors instead of Errors * Move obj destructuring inside for loop * Remove "computed" from ClassPrivateMethod type def --- .../babel-generator/src/generators/classes.js | 6 + .../types/ClassBody-MethodDefinition/input.js | 11 +- .../ClassBody-MethodDefinition/options.json | 1 + .../ClassBody-MethodDefinition/output.js | 16 +++ packages/babel-helpers/src/helpers.js | 15 +++ .../src/features.js | 36 +++++- .../babel-plugin-class-features/src/fields.js | 122 ++++++++++++++---- .../babel-plugin-class-features/src/index.js | 6 +- .../.npmignore | 3 + .../README.md | 19 +++ .../package.json | 25 ++++ .../src/index.js | 25 ++++ .../private-method-loose/assignment/exec.js | 11 ++ .../private-method-loose/assignment/input.js | 9 ++ .../private-method-loose/assignment/output.js | 15 +++ .../private-method-loose/context/exec.js | 41 ++++++ .../private-method-loose/context/input.js | 31 +++++ .../private-method-loose/context/output.js | 51 ++++++++ .../private-method-loose/exfiltrated/exec.js | 15 +++ .../private-method-loose/exfiltrated/input.js | 10 ++ .../exfiltrated/output.js | 18 +++ .../private-method-loose/options.json | 15 +++ .../private-method/assignment/exec.js | 11 ++ .../private-method/assignment/input.js | 9 ++ .../private-method/assignment/output.js | 15 +++ .../fixtures/private-method/context/exec.js | 41 ++++++ .../fixtures/private-method/context/input.js | 31 +++++ .../fixtures/private-method/context/output.js | 50 +++++++ .../private-method/exfiltrated/exec.js | 15 +++ .../private-method/exfiltrated/input.js | 10 ++ .../private-method/exfiltrated/output.js | 17 +++ .../test/fixtures/private-method/options.json | 15 +++ .../test/index.js | 3 + .../src/index.js | 6 +- .../src/asserts/generated/index.js | 6 + .../src/builders/generated/index.js | 4 + .../babel-types/src/definitions/es2015.js | 1 + .../src/definitions/experimental.js | 27 +++- .../src/validators/generated/index.js | 21 ++- .../src/validators/isReferenced.js | 1 + 40 files changed, 746 insertions(+), 38 deletions(-) create mode 100644 packages/babel-generator/test/fixtures/types/ClassBody-MethodDefinition/options.json create mode 100644 packages/babel-plugin-proposal-private-methods/.npmignore create mode 100644 packages/babel-plugin-proposal-private-methods/README.md create mode 100644 packages/babel-plugin-proposal-private-methods/package.json create mode 100644 packages/babel-plugin-proposal-private-methods/src/index.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/assignment/exec.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/assignment/input.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/assignment/output.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/context/exec.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/context/input.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/context/output.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/exec.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/input.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/output.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/options.json create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/assignment/exec.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/assignment/input.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/assignment/output.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/context/exec.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/context/input.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/context/output.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/exec.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/input.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/output.js create mode 100644 packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/options.json create mode 100644 packages/babel-plugin-proposal-private-methods/test/index.js diff --git a/packages/babel-generator/src/generators/classes.js b/packages/babel-generator/src/generators/classes.js index 22780dc3f03b..e8c62339a6d2 100644 --- a/packages/babel-generator/src/generators/classes.js +++ b/packages/babel-generator/src/generators/classes.js @@ -140,6 +140,12 @@ export function ClassMethod(node: Object) { this.print(node.body, node); } +export function ClassPrivateMethod(node: Object) { + this._classMethodHead(node); + this.space(); + this.print(node.body, node); +} + export function _classMethodHead(node) { this.printJoin(node.decorators, node); diff --git a/packages/babel-generator/test/fixtures/types/ClassBody-MethodDefinition/input.js b/packages/babel-generator/test/fixtures/types/ClassBody-MethodDefinition/input.js index bec3598e321c..e1a7074425ea 100644 --- a/packages/babel-generator/test/fixtures/types/ClassBody-MethodDefinition/input.js +++ b/packages/babel-generator/test/fixtures/types/ClassBody-MethodDefinition/input.js @@ -5,6 +5,15 @@ class Foo { get foo() {} set foo(bar) {} + async #foo() {} + #foo() {} + get #foo() {} + set #foo(bar) {} + * #foo() {} + async * #foo() {} + get #bar() {} + set #baz(taz) {} + static async foo() {} static foo() {} static ["foo"]() {} @@ -52,4 +61,4 @@ class Foo { get static () {} -} +} \ No newline at end of file diff --git a/packages/babel-generator/test/fixtures/types/ClassBody-MethodDefinition/options.json b/packages/babel-generator/test/fixtures/types/ClassBody-MethodDefinition/options.json new file mode 100644 index 000000000000..894b1b617167 --- /dev/null +++ b/packages/babel-generator/test/fixtures/types/ClassBody-MethodDefinition/options.json @@ -0,0 +1 @@ +{ "plugins": ["classPrivateMethods", "asyncGenerators"] } diff --git a/packages/babel-generator/test/fixtures/types/ClassBody-MethodDefinition/output.js b/packages/babel-generator/test/fixtures/types/ClassBody-MethodDefinition/output.js index 4676bff086d0..fbb5a3097e73 100644 --- a/packages/babel-generator/test/fixtures/types/ClassBody-MethodDefinition/output.js +++ b/packages/babel-generator/test/fixtures/types/ClassBody-MethodDefinition/output.js @@ -9,6 +9,22 @@ class Foo { set foo(bar) {} + async #foo() {} + + #foo() {} + + get #foo() {} + + set #foo(bar) {} + + *#foo() {} + + async *#foo() {} + + get #bar() {} + + set #baz(taz) {} + static async foo() {} static foo() {} diff --git a/packages/babel-helpers/src/helpers.js b/packages/babel-helpers/src/helpers.js index 5549bfde42a7..4c3c896fcc48 100644 --- a/packages/babel-helpers/src/helpers.js +++ b/packages/babel-helpers/src/helpers.js @@ -1739,3 +1739,18 @@ helpers.decorate = helper("7.1.5")` return constructor; } `; + +helpers.classPrivateMethodGet = helper("7.1.6")` + export default function _classPrivateMethodGet(receiver, privateSet, fn) { + if (!privateSet.has(receiver)) { + throw new TypeError("attempted to get private field on non-instance"); + } + return fn; + } +`; + +helpers.classPrivateMethodSet = helper("7.1.6")` + export default function _classPrivateMethodSet() { + throw new TypeError("attempted to reassign private method"); + } +`; diff --git a/packages/babel-plugin-class-features/src/features.js b/packages/babel-plugin-class-features/src/features.js index d777182852d7..79e314d830e5 100644 --- a/packages/babel-plugin-class-features/src/features.js +++ b/packages/babel-plugin-class-features/src/features.js @@ -39,19 +39,43 @@ export function isLoose(file, feature) { } export function verifyUsedFeatures(path, file) { + if (hasDecorators(path) && !hasFeature(file, FEATURES.decorators)) { + throw path.buildCodeFrameError("Decorators are not enabled."); + } + if (hasFeature(file, FEATURES.decorators)) { throw new Error( "@babel/plugin-class-features doesn't support decorators yet.", ); } - if (hasFeature(file, FEATURES.privateMethods)) { - throw new Error( - "@babel/plugin-class-features doesn't support private methods yet.", - ); + + if (path.isClassPrivateMethod()) { + if (!hasFeature(file, FEATURES.privateMethods)) { + throw path.buildCodeFrameError("Class private methods are not enabled."); + } + + if (path.node.static) { + throw path.buildCodeFrameError( + "@babel/plugin-class-features doesn't support class static private methods yet.", + ); + } + + if (path.node.kind !== "method") { + throw path.buildCodeFrameError( + "@babel/plugin-class-features doesn't support class private accessors yet.", + ); + } } - if (hasDecorators(path) && !hasFeature(file, FEATURES.decorators)) { - throw path.buildCodeFrameError("Decorators are not enabled."); + if ( + hasFeature(file, FEATURES.privateMethods) && + hasFeature(file, FEATURES.fields) && + isLoose(file, FEATURES.privateMethods) !== isLoose(file, FEATURES.fields) + ) { + throw path.buildCodeFrameError( + "'loose' mode configuration must be the same for both @babel/plugin-proposal-class-properties " + + "and @babel/plugin-proposal-private-methods", + ); } if (path.isProperty()) { diff --git a/packages/babel-plugin-class-features/src/fields.js b/packages/babel-plugin-class-features/src/fields.js index 0ed9329cb5d4..59511ebc7e38 100644 --- a/packages/babel-plugin-class-features/src/fields.js +++ b/packages/babel-plugin-class-features/src/fields.js @@ -11,6 +11,10 @@ export function buildPrivateNamesMap(props) { privateNamesMap.set(name, { id: prop.scope.generateUidIdentifier(name), static: !!prop.node.static, + method: prop.isClassPrivateMethod(), + methodId: prop.isClassPrivateMethod() + ? prop.scope.generateUidIdentifier(name) + : undefined, }); } } @@ -20,20 +24,22 @@ export function buildPrivateNamesMap(props) { export function buildPrivateNamesNodes(privateNamesMap, loose, state) { const initNodes = []; - for (const [name, { id, static: isStatic }] of privateNamesMap) { - // In loose mode, both static and instance fields hare transpiled using a + for (const [name, value] of privateNamesMap) { + // In loose mode, both static and instance fields are transpiled using a // secret non-enumerable property. Hence, we also need to generate that - // key (using the classPrivateFieldLooseKey helper) in loose mode. + // key (using the classPrivateFieldLooseKey helper). // In spec mode, only instance fields need a "private name" initializer - // (the WeakMap), becase static fields are directly assigned to a variable - // in the buildPrivateStaticFieldInitSpec function. - + // because static fields are directly assigned to a variable in the + // buildPrivateStaticFieldInitSpec function. + const { id, static: isStatic, method: isMethod } = value; if (loose) { initNodes.push( template.statement.ast` var ${id} = ${state.addHelper("classPrivateFieldLooseKey")}("${name}") `, ); + } else if (isMethod && !isStatic) { + initNodes.push(template.statement.ast`var ${id} = new WeakSet();`); } else if (!isStatic) { initNodes.push(template.statement.ast`var ${id} = new WeakMap();`); } @@ -42,7 +48,7 @@ export function buildPrivateNamesNodes(privateNamesMap, loose, state) { return initNodes; } -// Traverses the class scope, handling private name references. If an inner +// Traverses the class scope, handling private name references. If an inner // class redeclares the same private name, it will hand off traversal to the // restricted visitor (which doesn't traverse the inner class's inner scope). const privateNameVisitor = { @@ -61,7 +67,9 @@ const privateNameVisitor = { const body = path.get("body.body"); for (const prop of body) { - if (!prop.isClassPrivateProperty()) continue; + if (!prop.isPrivate()) { + continue; + } if (!privateNamesMap.has(prop.node.key.id.name)) continue; // This class redeclares the private name. @@ -108,13 +116,24 @@ const privateNameHandlerSpec = { get(member) { const { classRef, privateNamesMap, file } = this; const { name } = member.node.property.id; - const { id, static: isStatic } = privateNamesMap.get(name); - - if (isStatic) { + const { + id, + static: isStatic, + method: isMethod, + methodId, + } = privateNamesMap.get(name); + + if (isStatic && !isMethod) { return t.callExpression( file.addHelper("classStaticPrivateFieldSpecGet"), [this.receiver(member), t.cloneNode(classRef), t.cloneNode(id)], ); + } else if (isMethod) { + return t.callExpression(file.addHelper("classPrivateMethodGet"), [ + this.receiver(member), + t.cloneNode(id), + t.cloneNode(methodId), + ]); } else { return t.callExpression(file.addHelper("classPrivateFieldGet"), [ this.receiver(member), @@ -126,13 +145,17 @@ const privateNameHandlerSpec = { set(member, value) { const { classRef, privateNamesMap, file } = this; const { name } = member.node.property.id; - const { id, static: isStatic } = privateNamesMap.get(name); + const { id, static: isStatic, method: isMethod } = privateNamesMap.get( + name, + ); - if (isStatic) { + if (isStatic && !isMethod) { return t.callExpression( file.addHelper("classStaticPrivateFieldSpecSet"), [this.receiver(member), t.cloneNode(classRef), t.cloneNode(id), value], ); + } else if (isMethod) { + return t.callExpression(file.addHelper("classPrivateMethodSet"), []); } else { return t.callExpression(file.addHelper("classPrivateFieldSet"), [ this.receiver(member), @@ -231,6 +254,25 @@ function buildPrivateStaticFieldInitSpec(prop, privateNamesMap) { `; } +function buildPrivateMethodInitLoose(ref, prop, privateNamesMap) { + const { methodId, id } = privateNamesMap.get(prop.node.key.id.name); + + return template.statement.ast` + Object.defineProperty(${ref}, ${id}, { + // configurable is false by default + // enumerable is false by default + // writable is false by default + value: ${methodId.name} + }); + `; +} + +function buildPrivateInstanceMethodInitSpec(ref, prop, privateNamesMap) { + const { id } = privateNamesMap.get(prop.node.key.id.name); + + return template.statement.ast`${id}.add(${ref})`; +} + function buildPublicFieldInitLoose(ref, prop) { const { key, computed } = prop.node; const value = prop.node.value || prop.scope.buildUndefinedNode(); @@ -257,6 +299,16 @@ function buildPublicFieldInitSpec(ref, prop, state) { ); } +function buildPrivateInstanceMethodDeclaration(prop, privateNamesMap) { + const { methodId } = privateNamesMap.get(prop.node.key.id.name); + const { params, body } = prop.node; + const methodValue = t.functionExpression(methodId, params, body); + + return t.variableDeclaration("var", [ + t.variableDeclarator(methodId, methodValue), + ]); +} + export function buildFieldsInitNodes( ref, props, @@ -269,34 +321,34 @@ export function buildFieldsInitNodes( for (const prop of props) { const isStatic = prop.node.static; - const isPrivate = prop.isPrivate(); + const isPrivateField = prop.isClassPrivateProperty(); + const isPrivateMethod = prop.isClassPrivateMethod(); - // Pattern matching please switch (true) { - case isStatic && isPrivate && loose: + case isStatic && isPrivateField && loose: staticNodes.push( buildPrivateFieldInitLoose(t.cloneNode(ref), prop, privateNamesMap), ); break; - case isStatic && isPrivate && !loose: + case isStatic && isPrivateField && !loose: staticNodes.push( buildPrivateStaticFieldInitSpec(prop, privateNamesMap), ); break; - case isStatic && !isPrivate && loose: + case isStatic && !isPrivateField && loose: staticNodes.push(buildPublicFieldInitLoose(t.cloneNode(ref), prop)); break; - case isStatic && !isPrivate && !loose: + case isStatic && !isPrivateField && !loose: staticNodes.push( buildPublicFieldInitSpec(t.cloneNode(ref), prop, state), ); break; - case !isStatic && isPrivate && loose: + case !isStatic && isPrivateField && loose: instanceNodes.push( buildPrivateFieldInitLoose(t.thisExpression(), prop, privateNamesMap), ); break; - case !isStatic && isPrivate && !loose: + case !isStatic && isPrivateField && !loose: instanceNodes.push( buildPrivateInstanceFieldInitSpec( t.thisExpression(), @@ -305,10 +357,34 @@ export function buildFieldsInitNodes( ), ); break; - case !isStatic && !isPrivate && loose: + case !isStatic && isPrivateMethod && loose: + instanceNodes.push( + buildPrivateMethodInitLoose( + t.thisExpression(), + prop, + privateNamesMap, + ), + ); + staticNodes.push( + buildPrivateInstanceMethodDeclaration(prop, privateNamesMap), + ); + break; + case !isStatic && isPrivateMethod && !loose: + instanceNodes.push( + buildPrivateInstanceMethodInitSpec( + t.thisExpression(), + prop, + privateNamesMap, + ), + ); + staticNodes.push( + buildPrivateInstanceMethodDeclaration(prop, privateNamesMap), + ); + break; + case !isStatic && !isPrivateField && loose: instanceNodes.push(buildPublicFieldInitLoose(t.thisExpression(), prop)); break; - case !isStatic && !isPrivate && !loose: + case !isStatic && !isPrivateField && !loose: instanceNodes.push( buildPublicFieldInitSpec(t.thisExpression(), prop, state), ); diff --git a/packages/babel-plugin-class-features/src/index.js b/packages/babel-plugin-class-features/src/index.js index fefa3b41a185..1109c7792fb4 100644 --- a/packages/babel-plugin-class-features/src/index.js +++ b/packages/babel-plugin-class-features/src/index.js @@ -75,7 +75,6 @@ export default declare((api, options) => { enableFeature(this.file, FEATURES.fields, fields.loose); } if (privateMethods.enabled) { - throw new Error("Private methods are not supported yet"); enableFeature(this.file, FEATURES.privateMethods); } if (decorators.enabled) { @@ -107,7 +106,7 @@ export default declare((api, options) => { computedPaths.push(path); } - if (path.isClassPrivateProperty()) { + if (path.isPrivate()) { const { name } = path.node.key.id; if (privateNames.has(name)) { @@ -116,7 +115,7 @@ export default declare((api, options) => { privateNames.add(name); } - if (path.isProperty()) { + if (path.isProperty() || path.isClassPrivateMethod()) { props.push(path); } else if (path.isClassMethod({ kind: "constructor" })) { constructor = path; @@ -131,7 +130,6 @@ export default declare((api, options) => { nameFunction(path); ref = path.scope.generateUidIdentifier("class"); } else { - // path.isClassDeclaration() && path.node.id ref = path.node.id; } diff --git a/packages/babel-plugin-proposal-private-methods/.npmignore b/packages/babel-plugin-proposal-private-methods/.npmignore new file mode 100644 index 000000000000..f9806945836e --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/.npmignore @@ -0,0 +1,3 @@ +src +test +*.log diff --git a/packages/babel-plugin-proposal-private-methods/README.md b/packages/babel-plugin-proposal-private-methods/README.md new file mode 100644 index 000000000000..f4b7e50b120f --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/README.md @@ -0,0 +1,19 @@ +# @babel/plugin-proposal-private-methods + +> This plugin transforms private class methods + +See our website [@babel/plugin-proposal-private-methods](https://babeljs.io/docs/en/next/babel-plugin-proposal-private-methods.html) for more information. + +## Install + +Using npm: + +```sh +npm install --save-dev @babel/plugin-proposal-private-methods +``` + +or using yarn: + +```sh +yarn add @babel/plugin-proposal-private-methods --dev +``` diff --git a/packages/babel-plugin-proposal-private-methods/package.json b/packages/babel-plugin-proposal-private-methods/package.json new file mode 100644 index 000000000000..989b8015f46a --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/package.json @@ -0,0 +1,25 @@ +{ + "name": "@babel/plugin-proposal-private-methods", + "version": "7.1.0", + "description": "This plugin transforms private class methods", + "repository": "https://github.com/babel/babel/tree/master/packages/babel-plugin-proposal-private-methods", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "main": "lib/index.js", + "keywords": [ + "babel-plugin" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-class-features": "^7.1.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "devDependencies": { + "@babel/core": "^7.0.0", + "@babel/helper-plugin-test-runner": "^7.0.0" + } +} diff --git a/packages/babel-plugin-proposal-private-methods/src/index.js b/packages/babel-plugin-proposal-private-methods/src/index.js new file mode 100644 index 000000000000..94b19e4e75c6 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/src/index.js @@ -0,0 +1,25 @@ +import { declare } from "@babel/helper-plugin-utils"; +import pluginClassFeatures, { + enableFeature, + FEATURES, +} from "@babel/plugin-class-features"; + +export default declare((api, options) => { + api.assertVersion(7); + + const { loose } = options; + + return { + name: "proposal-private-methods", + + inherits: pluginClassFeatures, + + manipulateOptions(opts, parserOpts) { + parserOpts.plugins.push("classPrivateMethods"); + }, + + pre() { + enableFeature(this.file, FEATURES.privateMethods, loose); + }, + }; +}); diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/assignment/exec.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/assignment/exec.js new file mode 100644 index 000000000000..4c1324aeeb62 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/assignment/exec.js @@ -0,0 +1,11 @@ +class Foo { + constructor() { + this.publicField = this.#privateMethod(); + } + + #privateMethod() { + return 42; + } + } + + expect((new Foo).publicField).toEqual(42); diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/assignment/input.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/assignment/input.js new file mode 100644 index 000000000000..e55a8afdd3ad --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/assignment/input.js @@ -0,0 +1,9 @@ +class Foo { + constructor() { + this.publicField = this.#privateMethod(); + } + + #privateMethod() { + return 42; + } +} \ No newline at end of file diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/assignment/output.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/assignment/output.js new file mode 100644 index 000000000000..075d1451ed24 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/assignment/output.js @@ -0,0 +1,15 @@ +var Foo = function Foo() { + "use strict"; + + babelHelpers.classCallCheck(this, Foo); + Object.defineProperty(this, _privateMethod, { + value: _privateMethod2 + }); + this.publicField = babelHelpers.classPrivateFieldLooseBase(this, _privateMethod)[_privateMethod](); +}; + +var _privateMethod = babelHelpers.classPrivateFieldLooseKey("privateMethod"); + +var _privateMethod2 = function _privateMethod2() { + return 42; +}; diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/context/exec.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/context/exec.js new file mode 100644 index 000000000000..6c18bf53c193 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/context/exec.js @@ -0,0 +1,41 @@ +class Foo { + constructor(status) { + this.status = status; + expect(() => this.#getStatus = null).toThrow(TypeError); + } + + #getStatus() { + return this.status; + } + + getCurrentStatus() { + return this.#getStatus(); + } + + setCurrentStatus(newStatus) { + this.status = newStatus; + } + + getFakeStatus(fakeStatus) { + const getStatus = this.#getStatus; + return function () { + return getStatus.call({ status: fakeStatus }); + }; + } + + getFakeStatusFunc() { + return { + status: 'fake-status', + getFakeStatus: this.#getStatus, + }; + } + } + + const f = new Foo('inactive'); + expect(f.getCurrentStatus()).toBe('inactive'); + + f.setCurrentStatus('new-status'); + expect(f.getCurrentStatus()).toBe('new-status'); + + expect(f.getFakeStatus('fake')()).toBe('fake'); + expect(f.getFakeStatusFunc().getFakeStatus()).toBe('fake-status'); diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/context/input.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/context/input.js new file mode 100644 index 000000000000..67a8ab68a682 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/context/input.js @@ -0,0 +1,31 @@ +class Foo { + constructor(status) { + this.status = status; + } + + #getStatus() { + return this.status; + } + + getCurrentStatus() { + return this.#getStatus(); + } + + setCurrentStatus(newStatus) { + this.status = newStatus; + } + + getFakeStatus(fakeStatus) { + const fakeGetStatus = this.#getStatus; + return function() { + return fakeGetStatus.call({ status: fakeStatus }); + }; + } + + getFakeStatusFunc() { + return { + status: 'fake-status', + getFakeStatus: this.#getStatus, + }; + } +} \ No newline at end of file diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/context/output.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/context/output.js new file mode 100644 index 000000000000..96fb6f907354 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/context/output.js @@ -0,0 +1,51 @@ +var Foo = +/*#__PURE__*/ +function () { + "use strict"; + + function Foo(status) { + babelHelpers.classCallCheck(this, Foo); + Object.defineProperty(this, _getStatus, { + value: _getStatus2 + }); + this.status = status; + } + + babelHelpers.createClass(Foo, [{ + key: "getCurrentStatus", + value: function getCurrentStatus() { + return babelHelpers.classPrivateFieldLooseBase(this, _getStatus)[_getStatus](); + } + }, { + key: "setCurrentStatus", + value: function setCurrentStatus(newStatus) { + this.status = newStatus; + } + }, { + key: "getFakeStatus", + value: function getFakeStatus(fakeStatus) { + var fakeGetStatus = babelHelpers.classPrivateFieldLooseBase(this, _getStatus)[_getStatus]; + + return function () { + return fakeGetStatus.call({ + status: fakeStatus + }); + }; + } + }, { + key: "getFakeStatusFunc", + value: function getFakeStatusFunc() { + return { + status: 'fake-status', + getFakeStatus: babelHelpers.classPrivateFieldLooseBase(this, _getStatus)[_getStatus] + }; + } + }]); + return Foo; +}(); + +var _getStatus = babelHelpers.classPrivateFieldLooseKey("getStatus"); + +var _getStatus2 = function _getStatus2() { + return this.status; +}; diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/exec.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/exec.js new file mode 100644 index 000000000000..659a3e891085 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/exec.js @@ -0,0 +1,15 @@ +let exfiltrated; +class Foo { + #privateMethod() {} + + constructor() { + if (exfiltrated === undefined) { + exfiltrated = this.#privateMethod; + } + expect(exfiltrated).toStrictEqual(this.#privateMethod); + } +} + +new Foo(); +// check for private method function object equality +new Foo(); \ No newline at end of file diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/input.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/input.js new file mode 100644 index 000000000000..91bf8510d814 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/input.js @@ -0,0 +1,10 @@ +let exfiltrated; +class Foo { + #privateMethod() {} + + constructor() { + if (exfiltrated === undefined) { + exfiltrated = this.#privateMethod; + } + } +} \ No newline at end of file diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/output.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/output.js new file mode 100644 index 000000000000..5ac64080fedd --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/output.js @@ -0,0 +1,18 @@ +var exfiltrated; + +var Foo = function Foo() { + "use strict"; + + babelHelpers.classCallCheck(this, Foo); + Object.defineProperty(this, _privateMethod, { + value: _privateMethod2 + }); + + if (exfiltrated === undefined) { + exfiltrated = babelHelpers.classPrivateFieldLooseBase(this, _privateMethod)[_privateMethod]; + } +}; + +var _privateMethod = babelHelpers.classPrivateFieldLooseKey("privateMethod"); + +var _privateMethod2 = function _privateMethod2() {}; diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/options.json b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/options.json new file mode 100644 index 000000000000..d5a412759768 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/options.json @@ -0,0 +1,15 @@ +{ + "plugins": [ + [ + "external-helpers", + { + "helperVersion": "7.1.6" + } + ], + ["proposal-private-methods", { "loose": true }], + ["proposal-class-properties", { "loose": true }], + "transform-classes", + "transform-block-scoping", + "syntax-class-properties" + ] +} diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/assignment/exec.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/assignment/exec.js new file mode 100644 index 000000000000..24633c66730d --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/assignment/exec.js @@ -0,0 +1,11 @@ +class Foo { + constructor() { + this.publicField = this.#privateMethod(); + } + + #privateMethod() { + return 42; + } + } + + expect((new Foo).publicField).toEqual(42); \ No newline at end of file diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/assignment/input.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/assignment/input.js new file mode 100644 index 000000000000..e55a8afdd3ad --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/assignment/input.js @@ -0,0 +1,9 @@ +class Foo { + constructor() { + this.publicField = this.#privateMethod(); + } + + #privateMethod() { + return 42; + } +} \ No newline at end of file diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/assignment/output.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/assignment/output.js new file mode 100644 index 000000000000..8951f902f0ba --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/assignment/output.js @@ -0,0 +1,15 @@ +var Foo = function Foo() { + "use strict"; + + babelHelpers.classCallCheck(this, Foo); + + _privateMethod.add(this); + + this.publicField = babelHelpers.classPrivateMethodGet(this, _privateMethod, _privateMethod2).call(this); +}; + +var _privateMethod = new WeakSet(); + +var _privateMethod2 = function _privateMethod2() { + return 42; +}; diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/context/exec.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/context/exec.js new file mode 100644 index 000000000000..6c18bf53c193 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/context/exec.js @@ -0,0 +1,41 @@ +class Foo { + constructor(status) { + this.status = status; + expect(() => this.#getStatus = null).toThrow(TypeError); + } + + #getStatus() { + return this.status; + } + + getCurrentStatus() { + return this.#getStatus(); + } + + setCurrentStatus(newStatus) { + this.status = newStatus; + } + + getFakeStatus(fakeStatus) { + const getStatus = this.#getStatus; + return function () { + return getStatus.call({ status: fakeStatus }); + }; + } + + getFakeStatusFunc() { + return { + status: 'fake-status', + getFakeStatus: this.#getStatus, + }; + } + } + + const f = new Foo('inactive'); + expect(f.getCurrentStatus()).toBe('inactive'); + + f.setCurrentStatus('new-status'); + expect(f.getCurrentStatus()).toBe('new-status'); + + expect(f.getFakeStatus('fake')()).toBe('fake'); + expect(f.getFakeStatusFunc().getFakeStatus()).toBe('fake-status'); diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/context/input.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/context/input.js new file mode 100644 index 000000000000..67a8ab68a682 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/context/input.js @@ -0,0 +1,31 @@ +class Foo { + constructor(status) { + this.status = status; + } + + #getStatus() { + return this.status; + } + + getCurrentStatus() { + return this.#getStatus(); + } + + setCurrentStatus(newStatus) { + this.status = newStatus; + } + + getFakeStatus(fakeStatus) { + const fakeGetStatus = this.#getStatus; + return function() { + return fakeGetStatus.call({ status: fakeStatus }); + }; + } + + getFakeStatusFunc() { + return { + status: 'fake-status', + getFakeStatus: this.#getStatus, + }; + } +} \ No newline at end of file diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/context/output.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/context/output.js new file mode 100644 index 000000000000..493abaf2bed5 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/context/output.js @@ -0,0 +1,50 @@ +var Foo = +/*#__PURE__*/ +function () { + "use strict"; + + function Foo(status) { + babelHelpers.classCallCheck(this, Foo); + + _getStatus.add(this); + + this.status = status; + } + + babelHelpers.createClass(Foo, [{ + key: "getCurrentStatus", + value: function getCurrentStatus() { + return babelHelpers.classPrivateMethodGet(this, _getStatus, _getStatus2).call(this); + } + }, { + key: "setCurrentStatus", + value: function setCurrentStatus(newStatus) { + this.status = newStatus; + } + }, { + key: "getFakeStatus", + value: function getFakeStatus(fakeStatus) { + var fakeGetStatus = babelHelpers.classPrivateMethodGet(this, _getStatus, _getStatus2); + return function () { + return fakeGetStatus.call({ + status: fakeStatus + }); + }; + } + }, { + key: "getFakeStatusFunc", + value: function getFakeStatusFunc() { + return { + status: 'fake-status', + getFakeStatus: babelHelpers.classPrivateMethodGet(this, _getStatus, _getStatus2) + }; + } + }]); + return Foo; +}(); + +var _getStatus = new WeakSet(); + +var _getStatus2 = function _getStatus2() { + return this.status; +}; diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/exec.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/exec.js new file mode 100644 index 000000000000..659a3e891085 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/exec.js @@ -0,0 +1,15 @@ +let exfiltrated; +class Foo { + #privateMethod() {} + + constructor() { + if (exfiltrated === undefined) { + exfiltrated = this.#privateMethod; + } + expect(exfiltrated).toStrictEqual(this.#privateMethod); + } +} + +new Foo(); +// check for private method function object equality +new Foo(); \ No newline at end of file diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/input.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/input.js new file mode 100644 index 000000000000..91bf8510d814 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/input.js @@ -0,0 +1,10 @@ +let exfiltrated; +class Foo { + #privateMethod() {} + + constructor() { + if (exfiltrated === undefined) { + exfiltrated = this.#privateMethod; + } + } +} \ No newline at end of file diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/output.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/output.js new file mode 100644 index 000000000000..4847ebd09aac --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/output.js @@ -0,0 +1,17 @@ +var exfiltrated; + +var Foo = function Foo() { + "use strict"; + + babelHelpers.classCallCheck(this, Foo); + + _privateMethod.add(this); + + if (exfiltrated === undefined) { + exfiltrated = babelHelpers.classPrivateMethodGet(this, _privateMethod, _privateMethod2); + } +}; + +var _privateMethod = new WeakSet(); + +var _privateMethod2 = function _privateMethod2() {}; diff --git a/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/options.json b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/options.json new file mode 100644 index 000000000000..4f20acf59cc6 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/options.json @@ -0,0 +1,15 @@ +{ + "plugins": [ + [ + "external-helpers", + { + "helperVersion": "7.1.6" + } + ], + "proposal-private-methods", + "proposal-class-properties", + "transform-classes", + "transform-block-scoping", + "syntax-class-properties" + ] +} diff --git a/packages/babel-plugin-proposal-private-methods/test/index.js b/packages/babel-plugin-proposal-private-methods/test/index.js new file mode 100644 index 000000000000..1b534b8fc64a --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/index.js @@ -0,0 +1,3 @@ +import runner from "@babel/helper-plugin-test-runner"; + +runner(__dirname); diff --git a/packages/babel-plugin-syntax-class-properties/src/index.js b/packages/babel-plugin-syntax-class-properties/src/index.js index f5d26ed80aa9..113fcbf9a245 100644 --- a/packages/babel-plugin-syntax-class-properties/src/index.js +++ b/packages/babel-plugin-syntax-class-properties/src/index.js @@ -7,7 +7,11 @@ export default declare(api => { name: "syntax-class-properties", manipulateOptions(opts, parserOpts) { - parserOpts.plugins.push("classProperties", "classPrivateProperties"); + parserOpts.plugins.push( + "classProperties", + "classPrivateProperties", + "classPrivateMethods", + ); }, }; }); diff --git a/packages/babel-types/src/asserts/generated/index.js b/packages/babel-types/src/asserts/generated/index.js index 65cb2901e83c..e55c1e6f38f1 100644 --- a/packages/babel-types/src/asserts/generated/index.js +++ b/packages/babel-types/src/asserts/generated/index.js @@ -687,6 +687,12 @@ export function assertClassPrivateProperty( ): void { assert("ClassPrivateProperty", node, opts); } +export function assertClassPrivateMethod( + node: Object, + opts?: Object = {}, +): void { + assert("ClassPrivateMethod", node, opts); +} export function assertImport(node: Object, opts?: Object = {}): void { assert("Import", node, opts); } diff --git a/packages/babel-types/src/builders/generated/index.js b/packages/babel-types/src/builders/generated/index.js index 86ac361d3d76..fc4eb6285eea 100644 --- a/packages/babel-types/src/builders/generated/index.js +++ b/packages/babel-types/src/builders/generated/index.js @@ -620,6 +620,10 @@ export function ClassPrivateProperty(...args: Array): Object { return builder("ClassPrivateProperty", ...args); } export { ClassPrivateProperty as classPrivateProperty }; +export function ClassPrivateMethod(...args: Array): Object { + return builder("ClassPrivateMethod", ...args); +} +export { ClassPrivateMethod as classPrivateMethod }; export function Import(...args: Array): Object { return builder("Import", ...args); } diff --git a/packages/babel-types/src/definitions/es2015.js b/packages/babel-types/src/definitions/es2015.js index 1147acd86790..182dee469d83 100644 --- a/packages/babel-types/src/definitions/es2015.js +++ b/packages/babel-types/src/definitions/es2015.js @@ -87,6 +87,7 @@ defineType("ClassBody", { assertEach( assertNodeType( "ClassMethod", + "ClassPrivateMethod", "ClassProperty", "ClassPrivateProperty", "TSDeclareMethod", diff --git a/packages/babel-types/src/definitions/experimental.js b/packages/babel-types/src/definitions/experimental.js index e933712f9877..03865adfc4a3 100644 --- a/packages/babel-types/src/definitions/experimental.js +++ b/packages/babel-types/src/definitions/experimental.js @@ -5,7 +5,10 @@ import defineType, { assertValueType, chain, } from "./utils"; -import { classMethodOrPropertyCommon } from "./es2015"; +import { + classMethodOrPropertyCommon, + classMethodOrDeclareMethodCommon, +} from "./es2015"; defineType("AwaitExpression", { builder: ["argument"], @@ -131,6 +134,28 @@ defineType("ClassPrivateProperty", { }, }); +defineType("ClassPrivateMethod", { + builder: ["kind", "key", "params", "body", "static"], + visitor: [ + "key", + "params", + "body", + "decorators", + "returnType", + "typeParameters", + ], + aliases: ["Method", "Private", "Function"], + fields: { + ...classMethodOrDeclareMethodCommon, + key: { + validate: assertNodeType("PrivateName"), + }, + body: { + validate: assertNodeType("BlockStatement"), + }, + }, +}); + defineType("Import", { aliases: ["Expression"], }); diff --git a/packages/babel-types/src/validators/generated/index.js b/packages/babel-types/src/validators/generated/index.js index 41484fa04871..9b47f4feb9aa 100644 --- a/packages/babel-types/src/validators/generated/index.js +++ b/packages/babel-types/src/validators/generated/index.js @@ -2159,6 +2159,20 @@ export function isClassPrivateProperty(node: Object, opts?: Object): boolean { return false; } +export function isClassPrivateMethod(node: Object, opts?: Object): boolean { + if (!node) return false; + + const nodeType = node.type; + if (nodeType === "ClassPrivateMethod") { + if (typeof opts === "undefined") { + return true; + } else { + return shallowEqual(node, opts); + } + } + + return false; +} export function isImport(node: Object, opts?: Object): boolean { if (!node) return false; @@ -3492,7 +3506,8 @@ export function isFunction(node: Object, opts?: Object): boolean { "FunctionExpression" === nodeType || "ObjectMethod" === nodeType || "ArrowFunctionExpression" === nodeType || - "ClassMethod" === nodeType + "ClassMethod" === nodeType || + "ClassPrivateMethod" === nodeType ) { if (typeof opts === "undefined") { return true; @@ -3737,7 +3752,8 @@ export function isMethod(node: Object, opts?: Object): boolean { if ( nodeType === "Method" || "ObjectMethod" === nodeType || - "ClassMethod" === nodeType + "ClassMethod" === nodeType || + "ClassPrivateMethod" === nodeType ) { if (typeof opts === "undefined") { return true; @@ -4119,6 +4135,7 @@ export function isPrivate(node: Object, opts?: Object): boolean { if ( nodeType === "Private" || "ClassPrivateProperty" === nodeType || + "ClassPrivateMethod" === nodeType || "PrivateName" === nodeType ) { if (typeof opts === "undefined") { diff --git a/packages/babel-types/src/validators/isReferenced.js b/packages/babel-types/src/validators/isReferenced.js index 136cde829ede..b629e625ee4d 100644 --- a/packages/babel-types/src/validators/isReferenced.js +++ b/packages/babel-types/src/validators/isReferenced.js @@ -46,6 +46,7 @@ export default function isReferenced(node: Object, parent: Object): boolean { // no: class { NODE() {} } // yes: class { [NODE]() {} } case "ClassMethod": + case "ClassPrivateMethod": case "ObjectMethod": if (parent.key === node) { return !!parent.computed;