Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Private class methods stage 3 #8654

Merged
merged 12 commits into from Nov 29, 2018
6 changes: 6 additions & 0 deletions packages/babel-generator/src/generators/classes.js
Expand Up @@ -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);

Expand Down
Expand Up @@ -5,6 +5,15 @@ class Foo {
get foo() {}
set foo(bar) {}

async #foo() {}
#foo() {}
get #foo() {}
set #foo(bar) {}
tim-mc marked this conversation as resolved.
Show resolved Hide resolved
* #foo() {}
async * #foo() {}
get #bar() {}
set #baz(taz) {}

static async foo() {}
static foo() {}
static ["foo"]() {}
Expand Down Expand Up @@ -52,4 +61,4 @@ class Foo {
get
static
() {}
}
}
@@ -0,0 +1 @@
{ "plugins": ["classPrivateMethods", "asyncGenerators"] }
Expand Up @@ -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() {}
Expand Down
15 changes: 15 additions & 0 deletions packages/babel-helpers/src/helpers.js
Expand Up @@ -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");
}
`;
36 changes: 30 additions & 6 deletions packages/babel-plugin-class-features/src/features.js
Expand Up @@ -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()) {
Expand Down
122 changes: 99 additions & 23 deletions packages/babel-plugin-class-features/src/fields.js
Expand Up @@ -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,
});
}
}
Expand All @@ -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();`);
}
Expand All @@ -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 = {
Expand All @@ -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.
Expand Down Expand Up @@ -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(
tim-mc marked this conversation as resolved.
Show resolved Hide resolved
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),
Expand All @@ -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),
Expand Down Expand Up @@ -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();
Expand All @@ -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,
Expand All @@ -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(),
Expand All @@ -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),
);
Expand Down
6 changes: 2 additions & 4 deletions packages/babel-plugin-class-features/src/index.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)) {
Expand All @@ -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;
Expand All @@ -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;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/babel-plugin-proposal-private-methods/.npmignore
@@ -0,0 +1,3 @@
src
test
*.log