From 65cc88ff6bc43895a04fd0d84125dea04f5c680f Mon Sep 17 00:00:00 2001 From: Timothy McClure Date: Tue, 6 Nov 2018 21:17:18 -0500 Subject: [PATCH 01/12] Add private method syntax support --- .../babel-generator/src/generators/classes.js | 6 +++++ .../types/ClassBody-MethodDefinition/input.js | 11 +++++++- .../ClassBody-MethodDefinition/options.json | 1 + .../ClassBody-MethodDefinition/output.js | 16 +++++++++++ .../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 + 11 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 packages/babel-generator/test/fixtures/types/ClassBody-MethodDefinition/options.json 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-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..e0af4590595d 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", "computed", "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; From 0eada2497c877c9901dbe701af32ce5d90506bf7 Mon Sep 17 00:00:00 2001 From: Timothy McClure Date: Wed, 14 Nov 2018 08:23:50 -0500 Subject: [PATCH 02/12] Add private method spec support --- packages/babel-helpers/src/helpers.js | 15 +++ .../src/features.js | 9 +- .../babel-plugin-class-features/src/fields.js | 93 +++++++++++++++---- .../babel-plugin-class-features/src/index.js | 6 +- .../.npmignore | 3 + .../README.md | 19 ++++ .../package.json | 25 +++++ .../src/index.js | 25 +++++ .../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 | 13 +++ .../private-method/exfiltrated/input.js | 10 ++ .../private-method/exfiltrated/output.js | 17 ++++ .../test/fixtures/private-method/options.json | 15 +++ .../test/index.js | 3 + 20 files changed, 398 insertions(+), 27 deletions(-) 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/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-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..ad24dbf9aea0 100644 --- a/packages/babel-plugin-class-features/src/features.js +++ b/packages/babel-plugin-class-features/src/features.js @@ -44,10 +44,11 @@ export function verifyUsedFeatures(path, file) { "@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() && + !hasFeature(file, FEATURES.privateMethods) + ) { + throw path.buildCodeFrameError("Class private methods are not enabled."); } if (hasDecorators(path) && !hasFeature(file, FEATURES.decorators)) { diff --git a/packages/babel-plugin-class-features/src/fields.js b/packages/babel-plugin-class-features/src/fields.js index 0ed9329cb5d4..648a4a743ce6 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,13 +24,16 @@ 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, + { id, static: isStatic, method: isMethod }, + ] 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. if (loose) { initNodes.push( @@ -34,6 +41,8 @@ export function buildPrivateNamesNodes(privateNamesMap, loose, state) { var ${id} = ${state.addHelper("classPrivateFieldLooseKey")}("${name}") `, ); + } else if (isMethod) { + initNodes.push(template.statement.ast`var ${id} = new WeakSet();`); } else if (!isStatic) { initNodes.push(template.statement.ast`var ${id} = new WeakMap();`); } @@ -42,7 +51,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 +70,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 +119,24 @@ const privateNameHandlerSpec = { get(member) { const { classRef, privateNamesMap, file } = this; const { name } = member.node.property.id; - const { id, static: isStatic } = privateNamesMap.get(name); + const { + id, + static: isStatic, + method: isMethod, + methodId, + } = privateNamesMap.get(name); if (isStatic) { 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 +148,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) { 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 +257,12 @@ function buildPrivateStaticFieldInitSpec(prop, privateNamesMap) { `; } +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 +289,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 +311,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 +347,23 @@ export function buildFieldsInitNodes( ), ); break; - case !isStatic && !isPrivate && loose: + case !isStatic && isPrivateMethod && loose: + 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/options.json b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/options.json new file mode 100644 index 000000000000..3d8794240f3c --- /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", + ["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..d73ad144903f --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/exec.js @@ -0,0 +1,13 @@ +let exfiltrated; +class Foo { + #privateMethod() {} + + constructor() { + if (exfiltrated === undefined) { + exfiltrated = this.#privateMethod; + } + expect(exfiltrated).toStrictEqual(this.#privateMethod); + } +} + +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); From b8c581f1e0b69539e3f75db2eb9bde51742c6bbc Mon Sep 17 00:00:00 2001 From: Timothy McClure Date: Fri, 23 Nov 2018 19:09:04 -0500 Subject: [PATCH 03/12] Add private method loose support --- .../babel-plugin-class-features/src/fields.js | 24 +++++++++ .../private-method-loose/assignment/exec.js | 11 ++++ .../private-method-loose/assignment/input.js | 9 ++++ .../private-method-loose/assignment/output.js | 16 ++++++ .../private-method-loose/context/exec.js | 41 +++++++++++++++ .../private-method-loose/context/input.js | 31 +++++++++++ .../private-method-loose/context/output.js | 52 +++++++++++++++++++ .../private-method-loose/exfiltrated/exec.js | 13 +++++ .../private-method-loose/exfiltrated/input.js | 10 ++++ .../exfiltrated/output.js | 19 +++++++ 10 files changed, 226 insertions(+) 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 diff --git a/packages/babel-plugin-class-features/src/fields.js b/packages/babel-plugin-class-features/src/fields.js index 648a4a743ce6..076d3a8925a0 100644 --- a/packages/babel-plugin-class-features/src/fields.js +++ b/packages/babel-plugin-class-features/src/fields.js @@ -257,6 +257,19 @@ 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: false, + value: ${methodId.name} + }); + `; +} + function buildPrivateInstanceMethodInitSpec(ref, prop, privateNamesMap) { const { id } = privateNamesMap.get(prop.node.key.id.name); @@ -348,6 +361,17 @@ export function buildFieldsInitNodes( ); break; case !isStatic && isPrivateMethod && loose: + instanceNodes.push( + buildPrivateMethodInitLoose( + t.thisExpression(), + prop, + privateNamesMap, + ), + ); + staticNodes.push( + buildPrivateInstanceMethodDeclaration(prop, privateNamesMap), + ); + break; case !isStatic && isPrivateMethod && !loose: instanceNodes.push( buildPrivateInstanceMethodInitSpec( 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..669940f94991 --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/assignment/output.js @@ -0,0 +1,16 @@ +var Foo = function Foo() { + "use strict"; + + babelHelpers.classCallCheck(this, Foo); + Object.defineProperty(this, _privateMethod, { + writable: false, + 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..e0b936f3d31a --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/context/output.js @@ -0,0 +1,52 @@ +var Foo = +/*#__PURE__*/ +function () { + "use strict"; + + function Foo(status) { + babelHelpers.classCallCheck(this, Foo); + Object.defineProperty(this, _getStatus, { + writable: false, + 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..d73ad144903f --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/exec.js @@ -0,0 +1,13 @@ +let exfiltrated; +class Foo { + #privateMethod() {} + + constructor() { + if (exfiltrated === undefined) { + exfiltrated = this.#privateMethod; + } + expect(exfiltrated).toStrictEqual(this.#privateMethod); + } +} + +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..c4cdb50d819e --- /dev/null +++ b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/exfiltrated/output.js @@ -0,0 +1,19 @@ +var exfiltrated; + +var Foo = function Foo() { + "use strict"; + + babelHelpers.classCallCheck(this, Foo); + Object.defineProperty(this, _privateMethod, { + writable: false, + value: _privateMethod2 + }); + + if (exfiltrated === undefined) { + exfiltrated = babelHelpers.classPrivateFieldLooseBase(this, _privateMethod)[_privateMethod]; + } +}; + +var _privateMethod = babelHelpers.classPrivateFieldLooseKey("privateMethod"); + +var _privateMethod2 = function _privateMethod2() {}; From 3c3b039121cc4e6451be74d0e9c2ed9ea517320b Mon Sep 17 00:00:00 2001 From: Timothy McClure Date: Fri, 23 Nov 2018 19:36:34 -0500 Subject: [PATCH 04/12] Throw error if static private method is used --- packages/babel-plugin-class-features/src/features.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/babel-plugin-class-features/src/features.js b/packages/babel-plugin-class-features/src/features.js index ad24dbf9aea0..6ee6090b9490 100644 --- a/packages/babel-plugin-class-features/src/features.js +++ b/packages/babel-plugin-class-features/src/features.js @@ -44,6 +44,7 @@ export function verifyUsedFeatures(path, file) { "@babel/plugin-class-features doesn't support decorators yet.", ); } + if ( path.isClassPrivateMethod() && !hasFeature(file, FEATURES.privateMethods) @@ -51,6 +52,12 @@ export function verifyUsedFeatures(path, file) { throw path.buildCodeFrameError("Class private methods are not enabled."); } + if (path.isClassPrivateMethod() && path.node.static) { + throw new Error( + "@babel/plugin-class-features doesn't support class static private methods yet.", + ); + } + if (hasDecorators(path) && !hasFeature(file, FEATURES.decorators)) { throw path.buildCodeFrameError("Decorators are not enabled."); } From 59cd0310e5d3b3f0c2d137a08b078435b23183a5 Mon Sep 17 00:00:00 2001 From: Timothy McClure Date: Sat, 24 Nov 2018 10:31:53 -0500 Subject: [PATCH 05/12] Add more isStatic & isMethod checks --- packages/babel-plugin-class-features/src/fields.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/babel-plugin-class-features/src/fields.js b/packages/babel-plugin-class-features/src/fields.js index 076d3a8925a0..c56308d0e11f 100644 --- a/packages/babel-plugin-class-features/src/fields.js +++ b/packages/babel-plugin-class-features/src/fields.js @@ -41,7 +41,7 @@ export function buildPrivateNamesNodes(privateNamesMap, loose, state) { var ${id} = ${state.addHelper("classPrivateFieldLooseKey")}("${name}") `, ); - } else if (isMethod) { + } 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();`); @@ -126,7 +126,7 @@ const privateNameHandlerSpec = { methodId, } = privateNamesMap.get(name); - if (isStatic) { + if (isStatic && !isMethod) { return t.callExpression( file.addHelper("classStaticPrivateFieldSpecGet"), [this.receiver(member), t.cloneNode(classRef), t.cloneNode(id)], @@ -152,7 +152,7 @@ const privateNameHandlerSpec = { name, ); - if (isStatic) { + if (isStatic && !isMethod) { return t.callExpression( file.addHelper("classStaticPrivateFieldSpecSet"), [this.receiver(member), t.cloneNode(classRef), t.cloneNode(id), value], From 6d8b4e4d0b4f9e9c74e7a52ebe4119156eecbda3 Mon Sep 17 00:00:00 2001 From: Timothy McClure Date: Sat, 24 Nov 2018 10:36:38 -0500 Subject: [PATCH 06/12] Remove `writable:false` from private method inits `writable` is false by default. --- packages/babel-plugin-class-features/src/fields.js | 2 +- .../test/fixtures/private-method-loose/assignment/output.js | 1 - .../test/fixtures/private-method-loose/context/output.js | 1 - .../test/fixtures/private-method-loose/exfiltrated/output.js | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/babel-plugin-class-features/src/fields.js b/packages/babel-plugin-class-features/src/fields.js index c56308d0e11f..ee831f0194c9 100644 --- a/packages/babel-plugin-class-features/src/fields.js +++ b/packages/babel-plugin-class-features/src/fields.js @@ -264,7 +264,7 @@ function buildPrivateMethodInitLoose(ref, prop, privateNamesMap) { Object.defineProperty(${ref}, ${id}, { // configurable is false by default // enumerable is false by default - writable: false, + // writable is false by default value: ${methodId.name} }); `; 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 index 669940f94991..075d1451ed24 100644 --- 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 @@ -3,7 +3,6 @@ var Foo = function Foo() { babelHelpers.classCallCheck(this, Foo); Object.defineProperty(this, _privateMethod, { - writable: false, value: _privateMethod2 }); this.publicField = babelHelpers.classPrivateFieldLooseBase(this, _privateMethod)[_privateMethod](); 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 index e0b936f3d31a..96fb6f907354 100644 --- 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 @@ -6,7 +6,6 @@ function () { function Foo(status) { babelHelpers.classCallCheck(this, Foo); Object.defineProperty(this, _getStatus, { - writable: false, value: _getStatus2 }); this.status = status; 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 index c4cdb50d819e..5ac64080fedd 100644 --- 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 @@ -5,7 +5,6 @@ var Foo = function Foo() { babelHelpers.classCallCheck(this, Foo); Object.defineProperty(this, _privateMethod, { - writable: false, value: _privateMethod2 }); From 4b6cca8ee8ae630658a2792385919c91334272d6 Mon Sep 17 00:00:00 2001 From: Timothy McClure Date: Sat, 24 Nov 2018 11:31:20 -0500 Subject: [PATCH 07/12] Add private method func obj equality check --- .../test/fixtures/private-method-loose/exfiltrated/exec.js | 2 ++ .../test/fixtures/private-method/exfiltrated/exec.js | 2 ++ 2 files changed, 4 insertions(+) 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 index d73ad144903f..659a3e891085 100644 --- 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 @@ -10,4 +10,6 @@ class Foo { } } +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/exec.js b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/exfiltrated/exec.js index d73ad144903f..659a3e891085 100644 --- 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 @@ -10,4 +10,6 @@ class Foo { } } +new Foo(); +// check for private method function object equality new Foo(); \ No newline at end of file From 465810c058a13f49bbefd521eb9c928419d44d99 Mon Sep 17 00:00:00 2001 From: Timothy McClure Date: Sat, 24 Nov 2018 11:47:10 -0500 Subject: [PATCH 08/12] Throw if private accessor is used --- .../src/features.js | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/babel-plugin-class-features/src/features.js b/packages/babel-plugin-class-features/src/features.js index 6ee6090b9490..09ecc539001d 100644 --- a/packages/babel-plugin-class-features/src/features.js +++ b/packages/babel-plugin-class-features/src/features.js @@ -45,17 +45,22 @@ export function verifyUsedFeatures(path, file) { ); } - if ( - path.isClassPrivateMethod() && - !hasFeature(file, FEATURES.privateMethods) - ) { - throw path.buildCodeFrameError("Class private methods are not enabled."); - } + if (path.isClassPrivateMethod()) { + if (!hasFeature(file, FEATURES.privateMethods)) { + throw path.buildCodeFrameError("Class private methods are not enabled."); + } - if (path.isClassPrivateMethod() && path.node.static) { - throw new Error( - "@babel/plugin-class-features doesn't support class static private methods yet.", - ); + if (path.node.static) { + throw new Error( + "@babel/plugin-class-features doesn't support class static private methods yet.", + ); + } + + if (path.node.kind !== "method") { + throw new Error( + "@babel/plugin-class-features doesn't support class private accessors yet.", + ); + } } if (hasDecorators(path) && !hasFeature(file, FEATURES.decorators)) { From a84882e01b29e08069942ee780b587147cc8a12e Mon Sep 17 00:00:00 2001 From: Timothy McClure Date: Sat, 24 Nov 2018 15:24:16 -0500 Subject: [PATCH 09/12] Add check for fields === private method loose mode --- .../babel-plugin-class-features/src/features.js | 15 +++++++++++++-- .../fixtures/private-method-loose/options.json | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/babel-plugin-class-features/src/features.js b/packages/babel-plugin-class-features/src/features.js index 09ecc539001d..451bbabce1e4 100644 --- a/packages/babel-plugin-class-features/src/features.js +++ b/packages/babel-plugin-class-features/src/features.js @@ -39,6 +39,10 @@ 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.", @@ -63,8 +67,15 @@ export function verifyUsedFeatures(path, file) { } } - 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 new Error( + "'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-proposal-private-methods/test/fixtures/private-method-loose/options.json b/packages/babel-plugin-proposal-private-methods/test/fixtures/private-method-loose/options.json index 3d8794240f3c..d5a412759768 100644 --- 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 @@ -6,7 +6,7 @@ "helperVersion": "7.1.6" } ], - "proposal-private-methods", + ["proposal-private-methods", { "loose": true }], ["proposal-class-properties", { "loose": true }], "transform-classes", "transform-block-scoping", From 3e5f1a4fd44625b52aca68ebfcda1a26715a951c Mon Sep 17 00:00:00 2001 From: Timothy McClure Date: Wed, 28 Nov 2018 18:47:10 -0500 Subject: [PATCH 10/12] Throw buildCodeFrameErrors instead of Errors --- packages/babel-plugin-class-features/src/features.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/babel-plugin-class-features/src/features.js b/packages/babel-plugin-class-features/src/features.js index 451bbabce1e4..79e314d830e5 100644 --- a/packages/babel-plugin-class-features/src/features.js +++ b/packages/babel-plugin-class-features/src/features.js @@ -55,13 +55,13 @@ export function verifyUsedFeatures(path, file) { } if (path.node.static) { - throw new Error( + throw path.buildCodeFrameError( "@babel/plugin-class-features doesn't support class static private methods yet.", ); } if (path.node.kind !== "method") { - throw new Error( + throw path.buildCodeFrameError( "@babel/plugin-class-features doesn't support class private accessors yet.", ); } @@ -72,7 +72,7 @@ export function verifyUsedFeatures(path, file) { hasFeature(file, FEATURES.fields) && isLoose(file, FEATURES.privateMethods) !== isLoose(file, FEATURES.fields) ) { - throw new Error( + throw path.buildCodeFrameError( "'loose' mode configuration must be the same for both @babel/plugin-proposal-class-properties " + "and @babel/plugin-proposal-private-methods", ); From 2cba230839d1807c1c9cb876fdc426f8f8384aff Mon Sep 17 00:00:00 2001 From: Timothy McClure Date: Wed, 28 Nov 2018 18:47:54 -0500 Subject: [PATCH 11/12] Move obj destructuring inside for loop --- packages/babel-plugin-class-features/src/fields.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/babel-plugin-class-features/src/fields.js b/packages/babel-plugin-class-features/src/fields.js index ee831f0194c9..59511ebc7e38 100644 --- a/packages/babel-plugin-class-features/src/fields.js +++ b/packages/babel-plugin-class-features/src/fields.js @@ -24,17 +24,14 @@ export function buildPrivateNamesMap(props) { export function buildPrivateNamesNodes(privateNamesMap, loose, state) { const initNodes = []; - for (const [ - name, - { id, static: isStatic, method: isMethod }, - ] of privateNamesMap) { + 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 spec mode, only instance fields need a "private name" initializer // 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` From 9b0e842e59702c56c3511120bcac1169f6832ca1 Mon Sep 17 00:00:00 2001 From: Timothy McClure Date: Wed, 28 Nov 2018 18:48:18 -0500 Subject: [PATCH 12/12] Remove "computed" from ClassPrivateMethod type def --- packages/babel-types/src/definitions/experimental.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/babel-types/src/definitions/experimental.js b/packages/babel-types/src/definitions/experimental.js index e0af4590595d..03865adfc4a3 100644 --- a/packages/babel-types/src/definitions/experimental.js +++ b/packages/babel-types/src/definitions/experimental.js @@ -135,7 +135,7 @@ defineType("ClassPrivateProperty", { }); defineType("ClassPrivateMethod", { - builder: ["kind", "key", "params", "body", "computed", "static"], + builder: ["kind", "key", "params", "body", "static"], visitor: [ "key", "params",