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

feat: Implement privateFieldsAsSymbols assumption for classes #15435

Merged
merged 6 commits into from Feb 20, 2023
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
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"`);
fwienber marked this conversation as resolved.
Show resolved Hide resolved
}
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];
}