Skip to content

Commit

Permalink
feat: Implement privateFieldsAsSymbols assumption for classes (#15435)
Browse files Browse the repository at this point in the history
Co-authored-by: Nicol貌 Ribaudo <nicolo.ribaudo@gmail.com>
  • Loading branch information
fwienber and nicolo-ribaudo committed Feb 20, 2023
1 parent 79e1452 commit 3e60843
Show file tree
Hide file tree
Showing 119 changed files with 1,931 additions and 14 deletions.
1 change: 1 addition & 0 deletions packages/babel-core/src/config/validation/options.ts
Expand Up @@ -277,6 +277,7 @@ const knownAssumptions = [
"noIncompleteNsImportDetection",
"noNewArrows",
"objectRestNoSymbols",
"privateFieldsAsSymbols",
"privateFieldsAsProperties",
"pureGetters",
"setClassMethods",
Expand Down
20 changes: 13 additions & 7 deletions packages/babel-helper-create-class-features-plugin/src/fields.ts
Expand Up @@ -58,18 +58,22 @@ export function buildPrivateNamesMap(props: PropPath[]) {
export function buildPrivateNamesNodes(
privateNamesMap: PrivateNamesMap,
privateFieldsAsProperties: boolean,
privateFieldsAsSymbols: boolean,
state: File,
) {
const initNodes: t.Statement[] = [];

for (const [name, value] of privateNamesMap) {
// When the privateFieldsAsProperties assumption is enabled,
// 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.
// - When the privateFieldsAsProperties assumption is enabled,
// 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).
// - When the privateFieldsAsSymbols assumption is enabled,
// both static and instance fields are transpiled using a
// unique Symbol to define a non-enumerable property.
// - 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 { static: isStatic, method: isMethod, getId, setId } = value;
const isAccessor = getId || setId;
const id = t.cloneNode(value.id);
Expand All @@ -80,6 +84,8 @@ export function buildPrivateNamesNodes(
init = t.callExpression(state.addHelper("classPrivateFieldLooseKey"), [
t.stringLiteral(name),
]);
} else if (privateFieldsAsSymbols) {
init = t.callExpression(t.identifier("Symbol"), [t.stringLiteral(name)]);
} else if (!isStatic) {
init = t.newExpression(
t.identifier(!isMethod || isAccessor ? "WeakMap" : "WeakSet"),
Expand Down
26 changes: 21 additions & 5 deletions packages/babel-helper-create-class-features-plugin/src/index.ts
Expand Up @@ -48,19 +48,33 @@ export function createClassFeaturePlugin({
inherits,
}: Options): PluginObject {
const setPublicClassFields = api.assumption("setPublicClassFields");
const privateFieldsAsSymbols = api.assumption("privateFieldsAsSymbols");
const privateFieldsAsProperties = api.assumption("privateFieldsAsProperties");
const constantSuper = api.assumption("constantSuper");
const noDocumentAll = api.assumption("noDocumentAll");

if (privateFieldsAsProperties && privateFieldsAsSymbols) {
throw new Error(
`Cannot enable both the "privateFieldsAsProperties" and ` +
`"privateFieldsAsSymbols" assumptions as the same time.`,
);
}
const privateFieldsAsSymbolsOrProperties =
privateFieldsAsProperties || privateFieldsAsSymbols;

if (loose === true) {
const explicit = [];
type AssumptionName = Parameters<PluginAPI["assumption"]>[0];
const explicit: `"${AssumptionName}"`[] = [];

if (setPublicClassFields !== undefined) {
explicit.push(`"setPublicClassFields"`);
}
if (privateFieldsAsProperties !== undefined) {
explicit.push(`"privateFieldsAsProperties"`);
}
if (privateFieldsAsSymbols !== undefined) {
explicit.push(`"privateFieldsAsSymbols"`);
}
if (explicit.length !== 0) {
console.warn(
`[${name}]: You are using the "loose: true" option and you are` +
Expand All @@ -71,7 +85,7 @@ export function createClassFeaturePlugin({
` following top-level option:\n` +
`\t"assumptions": {\n` +
`\t\t"setPublicClassFields": true,\n` +
`\t\t"privateFieldsAsProperties": true\n` +
`\t\t"privateFieldsAsSymbols": true\n` +
`\t}`,
);
}
Expand Down Expand Up @@ -191,6 +205,7 @@ export function createClassFeaturePlugin({
const privateNamesNodes = buildPrivateNamesNodes(
privateNamesMap,
privateFieldsAsProperties ?? loose,
privateFieldsAsSymbols ?? false,
file,
);

Expand All @@ -199,7 +214,8 @@ export function createClassFeaturePlugin({
path,
privateNamesMap,
{
privateFieldsAsProperties: privateFieldsAsProperties ?? loose,
privateFieldsAsProperties:
privateFieldsAsSymbolsOrProperties ?? loose,
noDocumentAll,
innerBinding,
},
Expand Down Expand Up @@ -231,7 +247,7 @@ export function createClassFeaturePlugin({
privateNamesMap,
file,
setPublicClassFields ?? loose,
privateFieldsAsProperties ?? loose,
privateFieldsAsSymbolsOrProperties ?? loose,
constantSuper ?? loose,
innerBinding,
));
Expand All @@ -246,7 +262,7 @@ export function createClassFeaturePlugin({
privateNamesMap,
file,
setPublicClassFields ?? loose,
privateFieldsAsProperties ?? loose,
privateFieldsAsSymbolsOrProperties ?? loose,
constantSuper ?? loose,
innerBinding,
));
Expand Down
@@ -0,0 +1 @@
;
@@ -0,0 +1,8 @@
{
"plugins": ["proposal-class-properties"],
"assumptions": {
"privateFieldsAsProperties": true,
"privateFieldsAsSymbols": true
},
"throws": "Cannot enable both the \"privateFieldsAsProperties\" and \"privateFieldsAsSymbols\" assumptions as the same time."
}
@@ -1,5 +1,5 @@
[proposal-class-properties]: You are using the "loose: true" option and you are explicitly setting a value for the "setPublicClassFields" assumption. The "loose" option can cause incompatibilities with the other class features plugins, so it's recommended that you replace it with the following top-level option:
"assumptions": {
"setPublicClassFields": true,
"privateFieldsAsProperties": true
"privateFieldsAsSymbols": true
}
@@ -1,5 +1,5 @@
[proposal-class-properties]: You are using the "loose: true" option and you are explicitly setting a value for the "setPublicClassFields" assumption. The "loose" option can cause incompatibilities with the other class features plugins, so it's recommended that you replace it with the following top-level option:
"assumptions": {
"setPublicClassFields": true,
"privateFieldsAsProperties": true
"privateFieldsAsSymbols": true
}
@@ -0,0 +1,31 @@
class Cl {
#privateField = "top secret string";

constructor() {
this.publicField = "not secret string";
}

get #privateFieldValue() {
return this.#privateField;
}

set #privateFieldValue(newValue) {
this.#privateField = newValue;
}

publicGetPrivateField() {
return this.#privateFieldValue;
}

publicSetPrivateField(newValue) {
this.#privateFieldValue = newValue;
}
}

const cl = new Cl();

expect(cl.publicGetPrivateField()).toEqual("top secret string");

cl.publicSetPrivateField("new secret string");
expect(cl.publicGetPrivateField()).toEqual("new secret string");

@@ -0,0 +1,23 @@
class Cl {
#privateField = "top secret string";

constructor() {
this.publicField = "not secret string";
}

get #privateFieldValue() {
return this.#privateField;
}

set #privateFieldValue(newValue) {
this.#privateField = newValue;
}

publicGetPrivateField() {
return this.#privateFieldValue;
}

publicSetPrivateField(newValue) {
this.#privateFieldValue = newValue;
}
}
@@ -0,0 +1,27 @@
var _privateField = /*#__PURE__*/Symbol("privateField");
var _privateFieldValue = /*#__PURE__*/Symbol("privateFieldValue");
class Cl {
constructor() {
Object.defineProperty(this, _privateFieldValue, {
get: _get_privateFieldValue,
set: _set_privateFieldValue
});
Object.defineProperty(this, _privateField, {
writable: true,
value: "top secret string"
});
this.publicField = "not secret string";
}
publicGetPrivateField() {
return babelHelpers.classPrivateFieldLooseBase(this, _privateFieldValue)[_privateFieldValue];
}
publicSetPrivateField(newValue) {
babelHelpers.classPrivateFieldLooseBase(this, _privateFieldValue)[_privateFieldValue] = newValue;
}
}
function _get_privateFieldValue() {
return babelHelpers.classPrivateFieldLooseBase(this, _privateField)[_privateField];
}
function _set_privateFieldValue(newValue) {
babelHelpers.classPrivateFieldLooseBase(this, _privateField)[_privateField] = newValue;
}
@@ -0,0 +1,13 @@
class Cl {
#privateField = 0;

set #privateFieldValue(newValue) {
this.#privateField = newValue;
}

constructor() {
expect(this.#privateFieldValue).toBeUndefined();
}
}

const cl = new Cl();
@@ -0,0 +1,11 @@
class Cl {
#privateField = 0;

set #privateFieldValue(newValue) {
this.#privateField = newValue;
}

constructor() {
this.publicField = this.#privateFieldValue;
}
}
@@ -0,0 +1,18 @@
var _privateField = /*#__PURE__*/Symbol("privateField");
var _privateFieldValue = /*#__PURE__*/Symbol("privateFieldValue");
class Cl {
constructor() {
Object.defineProperty(this, _privateFieldValue, {
get: void 0,
set: _set_privateFieldValue
});
Object.defineProperty(this, _privateField, {
writable: true,
value: 0
});
this.publicField = babelHelpers.classPrivateFieldLooseBase(this, _privateFieldValue)[_privateFieldValue];
}
}
function _set_privateFieldValue(newValue) {
babelHelpers.classPrivateFieldLooseBase(this, _privateField)[_privateField] = newValue;
}
@@ -0,0 +1,11 @@
let foo;
class Cl {
set #foo(v) { return 1 }
test() {
foo = this.#foo = 2;
}
}

new Cl().test();

expect(foo).toBe(2);
@@ -0,0 +1,6 @@
{
"plugins": ["proposal-private-methods", "proposal-class-properties"],
"assumptions": {
"privateFieldsAsSymbols": true
}
}
@@ -0,0 +1,9 @@
class C {
/* before get a */
get #a() { return 42 };
/* after get a */

/* before set a */
set #a(v) {}
/* after set a */
}
@@ -0,0 +1,17 @@
var _a = /*#__PURE__*/Symbol("a");
class C {
constructor() {
/* before get a */
Object.defineProperty(this, _a, {
get: _get_a,
set: _set_a
});
}
/* after set a */
}
function _get_a() {
return 42;
}
/* after get a */
/* before set a */
function _set_a(v) {}
@@ -0,0 +1,14 @@
class Cl {
#privateField = 0;

get #privateFieldValue() {
return this.#privateField;
}

constructor() {
expect(() => this.#privateFieldValue = 1).toThrow(TypeError);
expect(() => ([this.#privateFieldValue] = [1])).toThrow(TypeError);
}
}

const cl = new Cl();
@@ -0,0 +1,12 @@
class Cl {
#privateField = 0;

get #privateFieldValue() {
return this.#privateField;
}

constructor() {
this.#privateFieldValue = 1;
([this.#privateFieldValue] = [1]);
}
}
@@ -0,0 +1,19 @@
var _privateField = /*#__PURE__*/Symbol("privateField");
var _privateFieldValue = /*#__PURE__*/Symbol("privateFieldValue");
class Cl {
constructor() {
Object.defineProperty(this, _privateFieldValue, {
get: _get_privateFieldValue,
set: void 0
});
Object.defineProperty(this, _privateField, {
writable: true,
value: 0
});
babelHelpers.classPrivateFieldLooseBase(this, _privateFieldValue)[_privateFieldValue] = 1;
[babelHelpers.classPrivateFieldLooseBase(this, _privateFieldValue)[_privateFieldValue]] = [1];
}
}
function _get_privateFieldValue() {
return babelHelpers.classPrivateFieldLooseBase(this, _privateField)[_privateField];
}

0 comments on commit 3e60843

Please sign in to comment.