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

Class Private Static Accessors #10217

Merged
merged 8 commits into from Sep 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -66,12 +66,6 @@ export function verifyUsedFeatures(path, file) {
if (!hasFeature(file, FEATURES.privateMethods)) {
throw path.buildCodeFrameError("Class private methods are not enabled.");
}

if (path.node.static && path.node.kind !== "method") {
throw path.buildCodeFrameError(
"@babel/plugin-class-features doesn't support class static private accessors yet.",
);
}
}

if (
Expand Down
154 changes: 87 additions & 67 deletions packages/babel-helper-create-class-features-plugin/src/fields.js
Expand Up @@ -44,14 +44,15 @@ export function buildPrivateNamesNodes(privateNamesMap, loose, state) {
// because static fields are directly assigned to a variable in the
// buildPrivateStaticFieldInitSpec function.
const { id, static: isStatic, method: isMethod, getId, setId } = value;
const isAccessor = getId || setId;
if (loose) {
initNodes.push(
template.statement.ast`
var ${id} = ${state.addHelper("classPrivateFieldLooseKey")}("${name}")
`,
);
} else if (isMethod && !isStatic) {
if (getId || setId) {
if (isAccessor) {
initNodes.push(template.statement.ast`var ${id} = new WeakMap();`);
} else {
initNodes.push(template.statement.ast`var ${id} = new WeakSet();`);
Expand Down Expand Up @@ -140,11 +141,13 @@ const privateNameHandlerSpec = {
getId,
setId,
} = privateNamesMap.get(name);
const isAccessor = getId || setId;

if (isStatic) {
const helperName = isMethod
? "classStaticPrivateMethodGet"
: "classStaticPrivateFieldSpecGet";
const helperName =
isMethod && !isAccessor
? "classStaticPrivateMethodGet"
: "classStaticPrivateFieldSpecGet";

return t.callExpression(file.addHelper(helperName), [
this.receiver(member),
Expand All @@ -154,7 +157,7 @@ const privateNameHandlerSpec = {
}

if (isMethod) {
if (getId || setId) {
if (isAccessor) {
return t.callExpression(file.addHelper("classPrivateFieldGet"), [
this.receiver(member),
t.cloneNode(id),
Expand All @@ -180,12 +183,15 @@ const privateNameHandlerSpec = {
static: isStatic,
method: isMethod,
setId,
getId,
} = privateNamesMap.get(name);
const isAccessor = getId || setId;

if (isStatic) {
const helperName = isMethod
? "classStaticPrivateMethodSet"
: "classStaticPrivateFieldSpecSet";
const helperName =
isMethod && !isAccessor
? "classStaticPrivateMethodSet"
: "classStaticPrivateFieldSpecSet";

return t.callExpression(file.addHelper(helperName), [
this.receiver(member),
Expand Down Expand Up @@ -300,9 +306,30 @@ function buildPrivateInstanceFieldInitSpec(ref, prop, privateNamesMap) {
}

function buildPrivateStaticFieldInitSpec(prop, privateNamesMap) {
const { id } = privateNamesMap.get(prop.node.key.id.name);
const value = prop.node.value || prop.scope.buildUndefinedNode();
const privateName = privateNamesMap.get(prop.node.key.id.name);
const { id, getId, setId, initAdded } = privateName;
const isAccessor = getId || setId;

if (!prop.isProperty() && (initAdded || !isAccessor)) return;

if (isAccessor) {
privateNamesMap.set(prop.node.key.id.name, {
...privateName,
initAdded: true,
});

return template.statement.ast`
var ${id.name} = {
// configurable is false by default
// enumerable is false by default
// writable is false by default
get: ${getId ? getId.name : prop.scope.buildUndefinedNode()},
set: ${setId ? setId.name : prop.scope.buildUndefinedNode()}
}
`;
}

const value = prop.node.value || prop.scope.buildUndefinedNode();
return template.statement.ast`
var ${id} = {
// configurable is false by default
Expand All @@ -328,76 +355,44 @@ function buildPrivateMethodInitLoose(ref, prop, privateNamesMap) {
});
`;
}

if (getId || setId) {
const isAccessor = getId || setId;
if (isAccessor) {
privateNamesMap.set(prop.node.key.id.name, {
...privateName,
initAdded: true,
});

if (getId && setId) {
return template.statement.ast`
Object.defineProperty(${ref}, ${id}, {
// configurable is false by default
// enumerable is false by default
// writable is false by default
get: ${getId.name},
set: ${setId.name}
});
`;
} else if (getId && !setId) {
return template.statement.ast`
Object.defineProperty(${ref}, ${id}, {
// configurable is false by default
// enumerable is false by default
// writable is false by default
get: ${getId.name}
});
`;
} else if (!getId && setId) {
return template.statement.ast`
Object.defineProperty(${ref}, ${id}, {
// configurable is false by default
// enumerable is false by default
// writable is false by default
set: ${setId.name}
});
`;
}
return template.statement.ast`
Object.defineProperty(${ref}, ${id}, {
// configurable is false by default
// enumerable is false by default
// writable is false by default
get: ${getId ? getId.name : prop.scope.buildUndefinedNode()},
set: ${setId ? setId.name : prop.scope.buildUndefinedNode()}
});
`;
}
}

function buildPrivateInstanceMethodInitSpec(ref, prop, privateNamesMap) {
const privateName = privateNamesMap.get(prop.node.key.id.name);
const { id, getId, setId, initAdded } = privateName;

if (initAdded) return;

if (getId || setId) {
const isAccessor = getId || setId;
if (isAccessor) {
privateNamesMap.set(prop.node.key.id.name, {
...privateName,
initAdded: true,
});

if (getId && setId) {
return template.statement.ast`
${id}.set(${ref}, {
get: ${getId.name},
set: ${setId.name}
});
`;
} else if (getId && !setId) {
return template.statement.ast`
${id}.set(${ref}, {
get: ${getId.name}
});
`;
} else if (!getId && setId) {
return template.statement.ast`
${id}.set(${ref}, {
set: ${setId.name}
});
`;
}
return template.statement.ast`
${id}.set(${ref}, {
get: ${getId ? getId.name : prop.scope.buildUndefinedNode()},
set: ${setId ? setId.name : prop.scope.buildUndefinedNode()}
});
`;
}
return template.statement.ast`${id}.add(${ref})`;
}
Expand Down Expand Up @@ -429,7 +424,29 @@ function buildPublicFieldInitSpec(ref, prop, state) {
}

function buildPrivateStaticMethodInitLoose(ref, prop, state, privateNamesMap) {
const { id, methodId } = privateNamesMap.get(prop.node.key.id.name);
const privateName = privateNamesMap.get(prop.node.key.id.name);
const { id, methodId, getId, setId, initAdded } = privateName;

if (initAdded) return;
tim-mc marked this conversation as resolved.
Show resolved Hide resolved

const isAccessor = getId || setId;
if (isAccessor) {
privateNamesMap.set(prop.node.key.id.name, {
...privateName,
initAdded: true,
});

return template.statement.ast`
Object.defineProperty(${ref}, ${id}, {
// configurable is false by default
// enumerable is false by default
// writable is false by default
get: ${getId ? getId.name : prop.scope.buildUndefinedNode()},
set: ${setId ? setId.name : prop.scope.buildUndefinedNode()}
})
`;
}

return template.statement.ast`
Object.defineProperty(${ref}, ${id}, {
// configurable is false by default
Expand Down Expand Up @@ -615,14 +632,14 @@ export function buildFieldsInitNodes(
case isStatic && isPrivate && isMethod && !loose:
needsClassRef = true;
staticNodes.push(
buildPrivateStaticFieldInitSpec(prop, privateNamesMap),
);
staticNodes.unshift(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why unshift?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use unshift so that the declaration of the getter and/or setter occurs before the usage of the accessor(s) (e.g., when they're defined as the values on an object). Here's what a test failure looks like if that's changed to push:

    babel/packages/babel-plugin-proposal-private-methods/test/fixtures/static-accessors/basic/exec.js: expect(received).toEqual(expected) // deep equality

    Expected: "top secret string"
    Received: undefined
       1 | class Cl {
       2 |   static getValue() {
       3 |     return babelHelpers.classStaticPrivateFieldSpecGet(Cl, Cl, _privateStaticFieldValue);
       4 |   }
       5 |
       6 |   static setValue() {
       7 |     babelHelpers.classStaticPrivateFieldSpecSet(Cl, Cl, _privateStaticFieldValue, "dank");
       8 |   }
       9 |
      10 | }
      11 |
      12 | var _PRIVATE_STATIC_FIELD = {
      13 |   writable: true,
      14 |   value: "top secret string"
      15 | };
      16 | var _privateStaticFieldValue = {
      17 |   get: _get_privateStaticFieldValue,
      18 |   set: _set_privateStaticFieldValue
      19 | };
      20 |
      21 | var _get_privateStaticFieldValue = function () {
      22 |   return babelHelpers.classStaticPrivateFieldSpecGet(Cl, Cl, _PRIVATE_STATIC_FIELD);
      23 | };
      24 |
      25 | var _set_privateStaticFieldValue = function (newValue) {
      26 |   babelHelpers.classStaticPrivateFieldSpecSet(Cl, Cl, _PRIVATE_STATIC_FIELD, `Updated: ${newValue}`);
      27 | };
      28 |
      29 | expect(Cl.getValue()).toEqual("top secret string");
      30 | Cl.setValue();
      31 | expect(Cl.getValue()).toEqual("Updated: dank");

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change the get/set variables to function declarations instead? If not, this is still fine.

buildPrivateMethodDeclaration(prop, privateNamesMap, loose),
);
break;
case isStatic && isPrivate && isMethod && loose:
needsClassRef = true;
staticNodes.push(
buildPrivateMethodDeclaration(prop, privateNamesMap, loose),
);
staticNodes.push(
buildPrivateStaticMethodInitLoose(
t.cloneNode(ref),
Expand All @@ -631,6 +648,9 @@ export function buildFieldsInitNodes(
privateNamesMap,
),
);
staticNodes.unshift(
buildPrivateMethodDeclaration(prop, privateNamesMap, loose),
);
break;
case isInstance && isPublic && isField && loose:
instanceNodes.push(buildPublicFieldInitLoose(t.thisExpression(), prop));
Expand All @@ -646,7 +666,7 @@ export function buildFieldsInitNodes(
}

return {
staticNodes,
staticNodes: staticNodes.filter(Boolean),
instanceNodes: instanceNodes.filter(Boolean),
wrapClass(path) {
for (const prop of props) {
Expand Down
20 changes: 14 additions & 6 deletions packages/babel-helpers/src/helpers.js
Expand Up @@ -1177,6 +1177,9 @@ helpers.classStaticPrivateFieldSpecGet = helper("7.0.2")`
if (receiver !== classConstructor) {
throw new TypeError("Private static access of wrong provenance");
}
if (descriptor.get) {
return descriptor.get.call(receiver);
}
return descriptor.value;
}
`;
Expand All @@ -1186,13 +1189,18 @@ helpers.classStaticPrivateFieldSpecSet = helper("7.0.2")`
if (receiver !== classConstructor) {
throw new TypeError("Private static access of wrong provenance");
}
if (!descriptor.writable) {
// This should only throw in strict mode, but class bodies are
// always strict and private fields can only be used inside
// class bodies.
throw new TypeError("attempted to set read only private field");
if (descriptor.set) {
descriptor.set.call(receiver, value);
} else {
if (!descriptor.writable) {
// This should only throw in strict mode, but class bodies are
// always strict and private fields can only be used inside
// class bodies.
throw new TypeError("attempted to set read only private field");
}
descriptor.value = value;
}
descriptor.value = value;

return value;
}
`;
Expand Down
@@ -1,6 +1,7 @@
class Cl {
constructor() {
Object.defineProperty(this, _privateFieldValue, {
get: void 0,
set: _set_privateFieldValue
});
Object.defineProperty(this, _privateField, {
Expand Down
@@ -1,7 +1,8 @@
class Cl {
constructor() {
Object.defineProperty(this, _privateFieldValue, {
get: _get_privateFieldValue
get: _get_privateFieldValue,
set: void 0
});
Object.defineProperty(this, _privateField, {
writable: true,
Expand Down
@@ -1,6 +1,7 @@
class Cl {
constructor() {
_privateFieldValue.set(this, {
get: void 0,
set: _set_privateFieldValue
});

Expand Down
@@ -1,7 +1,8 @@
class Cl {
constructor() {
_privateFieldValue.set(this, {
get: _get_privateFieldValue
get: _get_privateFieldValue,
set: void 0
});

_privateField.set(this, {
Expand Down
Expand Up @@ -20,18 +20,17 @@ var _getA = babelHelpers.classPrivateFieldLooseKey("getA");

var _getB = babelHelpers.classPrivateFieldLooseKey("getB");

var _getB2 = function _getB2() {
return this.b;
};

var _getA2 = function _getA2() {
return A.a;
};

Object.defineProperty(B, _getA, {
value: _getA2
});

var _getB2 = function _getB2() {
return this.b;
};

Object.defineProperty(B, _getB, {
value: _getB2
});
Expand Down
Expand Up @@ -16,12 +16,12 @@ class B extends A {

}

var _getA = function _getA() {
return babelHelpers.get(babelHelpers.getPrototypeOf(B), "a", this);
};

var _getB = function _getB() {
return this.b;
};

var _getA = function _getA() {
return babelHelpers.get(babelHelpers.getPrototypeOf(B), "a", this);
};

var [getA, getB] = B.extract();