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

Convert private methods to fields when they are supported #12250

Closed
200 changes: 161 additions & 39 deletions packages/babel-helper-create-class-features-plugin/src/features.js
Expand Up @@ -25,6 +25,7 @@ const featuresSameLoose = new Map([
// - node_modules
// - @babel-plugin-class-features
const featuresKey = "@babel/plugin-class-features/featuresKey";
const featuresIfFieldsKey = "@babel/plugin-class-features/featuresIfFieldsKey";
const looseKey = "@babel/plugin-class-features/looseKey";

// See https://github.com/babel/babel/issues/11622.
Expand All @@ -38,15 +39,15 @@ const looseKey = "@babel/plugin-class-features/looseKey";
const looseLowPriorityKey =
"@babel/plugin-class-features/looseLowPriorityKey/#__internal__@babel/preset-env__please-overwrite-loose-instead-of-throwing";

export function enableFeature(file, feature, loose) {
function enableFeatureOn(key, file, feature, loose) {
// We can't blindly enable the feature because, if it was already set,
// "loose" can't be changed, so that
// @babel/plugin-class-properties { loose: true }
// @babel/plugin-class-properties { loose: false }
// is transformed in loose mode.
// We only enabled the feature if it was previously disabled.
if (!hasFeature(file, feature) || canIgnoreLoose(file, feature)) {
file.set(featuresKey, file.get(featuresKey) | feature);
if (!mightHaveFeature(file, feature) || canIgnoreLoose(file, feature)) {
file.set(key, file.get(key) | feature);
if (
loose ===
"#__internal__@babel/preset-env__prefer-true-but-false-is-ok-if-it-prevents-an-error"
Expand All @@ -68,7 +69,7 @@ export function enableFeature(file, feature, loose) {
let higherPriorityPluginName: void | string;

for (const [mask, name] of featuresSameLoose) {
if (!hasFeature(file, mask)) continue;
if (!mightHaveFeature(file, mask)) continue;

const loose = isLoose(file, mask);

Expand All @@ -88,7 +89,10 @@ export function enableFeature(file, feature, loose) {

if (resolvedLoose !== undefined) {
for (const [mask, name] of featuresSameLoose) {
if (hasFeature(file, mask) && isLoose(file, mask) !== resolvedLoose) {
if (
mightHaveFeature(file, mask) &&
isLoose(file, mask) !== resolvedLoose
) {
setLoose(file, mask, resolvedLoose);
console.warn(
`Though the "loose" option was set to "${!resolvedLoose}" in your @babel/preset-env ` +
Expand All @@ -105,10 +109,36 @@ export function enableFeature(file, feature, loose) {
}
}

export function enableFeature(file, feature, loose) {
enableFeatureOn(featuresKey, file, feature, loose);
}

export function enableFeatureIfCompilingFields(file, feature, loose) {
enableFeatureOn(featuresIfFieldsKey, file, feature, loose);
}

function hasFeature(file, feature) {
return !!(file.get(featuresKey) & feature);
}

function hasFeatureIfCompilingFields(file, feature) {
return !!(file.get(featuresIfFieldsKey) & feature);
}

function mightHaveFeature(file, feature) {
return (
hasFeature(file, feature) || hasFeatureIfCompilingFields(file, feature)
);
}

function shouldCompile(file, feature) {
return (
hasFeature(file, feature) ||
(hasFeature(file, FEATURES.fields) &&
hasFeatureIfCompilingFields(file, feature))
);
}

export function isLoose(file, feature) {
return !!(file.get(looseKey) & feature);
}
Expand All @@ -125,52 +155,144 @@ function canIgnoreLoose(file, feature) {
}

export function verifyUsedFeatures(path, file) {
const used = {
fields: null,
privateElements: null,
privateMethods: null,
decorators: null,
privateIn: null,
staticBlock: null,
};

if (hasOwnDecorators(path.node)) {
if (!hasFeature(file, FEATURES.decorators)) {
throw path.buildCodeFrameError(
"Decorators are not enabled." +
"\nIf you are using " +
'["@babel/plugin-proposal-decorators", { "legacy": true }], ' +
'make sure it comes *before* "@babel/plugin-proposal-class-properties" ' +
"and enable loose mode, like so:\n" +
'\t["@babel/plugin-proposal-decorators", { "legacy": true }]\n' +
'\t["@babel/plugin-proposal-class-properties", { "loose": true }]',
);
used.decorators = path.get("decorators.0");
}

for (const elem of path.get("body.body")) {
if (hasOwnDecorators(elem.node)) {
used.decorators = used.decorators ?? elem.get("decorators.0");
}

if (path.isPrivate()) {
throw path.buildCodeFrameError(
`Private ${
path.isClassMethod() ? "methods" : "fields"
} in decorated classes are not supported yet.`,
if (elem.isPrivate()) {
if (elem.isMethod()) {
used.privateMethods = used.privateMethods ?? elem;
} else {
elem.assertProperty();
used.privateFields = used.privateFields ?? elem;
}
}

if (elem.isProperty()) {
used.fields = used.fields ?? elem;
}

if (elem.isStaticBlock?.()) {
throw elem.buildCodeFrameError(
`Incorrect plugin order, \`@babel/plugin-proposal-class-static-block\` should be placed before class features plugins
{
"plugins": [
"@babel/plugin-proposal-class-static-block",
"@babel/plugin-proposal-private-property-in-object",
"@babel/plugin-proposal-private-methods",
"@babel/plugin-proposal-class-properties",
]
}`,
);
}
}

// NOTE: We can't use path.isPrivateMethod() because it isn't supported in <7.2.0
if (path.isPrivate() && path.isMethod()) {
if (!hasFeature(file, FEATURES.privateMethods)) {
throw path.buildCodeFrameError("Class private methods are not enabled.");
}
if (used.privateElements) {
path.traverse({
BinaryExpression(path) {
if (path.node.operator === "in" && path.get("left").isPrivateName()) {
used.privateIn = path;
path.stop();
}
},
});
}

if (
path.isPrivateName() &&
path.parentPath.isBinaryExpression({
operator: "in",
left: path.node,
})
used.fields &&
used.privateMethods &&
hasFeature(file, FEATURES.fields) &&
!shouldCompile(file, FEATURES.privateMethods)
) {
if (!hasFeature(file, FEATURES.privateIn)) {
throw path.buildCodeFrameError(
"Private property in checks are not enabled.",
);
}
throw used.privateMethods.buildCodeFrameError(
"Class private methods are not enabled." +
"\nYou can enable @babel/plugin-proposal-private-methods to compile them.",
);
}

if (path.isProperty()) {
if (!hasFeature(file, FEATURES.fields)) {
throw path.buildCodeFrameError("Class fields are not enabled.");
}
if (
used.fields &&
used.privateMethods &&
!shouldCompile(file, FEATURES.fields) &&
hasFeature(file, FEATURES.privateMethods)
) {
throw used.fields.buildCodeFrameError(
"Class fields are not enabled." +
"\nYou can enable @babel/plugin-proposal-private-properties to compile them.",
);
}

if (used.decorators && !shouldCompile(file, FEATURES.decorators)) {
throw used.decorators.buildCodeFrameError(
"Decorators are not enabled." +
"\nIf you are using " +
'["@babel/plugin-proposal-decorators", { "legacy": true }], ' +
'make sure it comes *before* "@babel/plugin-proposal-class-properties" ' +
"and enable loose mode, like so:\n" +
'\t["@babel/plugin-proposal-decorators", { "legacy": true }]\n' +
'\t["@babel/plugin-proposal-class-properties", { "loose": true }]',
);
}

if (
used.decorators &&
used.privateElements &&
hasFeature(file, FEATURES.decorators)
) {
throw used.privateElements.buildCodeFrameError(
`Private ${
used.privateElements.isClassMethod() ? "methods" : "fields"
} in decorated classes are not supported yet.`,
);
}

if (
used.privateIn &&
hasFeature(file, FEATURES.privateIn) &&
!shouldCompile(file, FEATURES.fields)
) {
throw used.privateIn.buildCodeFrameError(
"It's not possible to compile '#private in obj' checks without compiling" +
" class fields.",
);
}

if (
used.privateIn &&
hasFeature(file, FEATURES.privateIn) &&
!shouldCompile(file, FEATURES.fields)
) {
throw used.privateIn.buildCodeFrameError(
"It's not possible to compile '#private in obj' checks without compiling" +
" class fields.",
);
}

if (
used.privateIn &&
!shouldCompile(file, FEATURES.privateIn) &&
hasFeature(file, FEATURES.fields)
) {
throw used.privateIn.buildCodeFrameError(
"Private property in checks are not enabled.",
);
}

return Object.keys(FEATURES).some(
feat => used[feat] && shouldCompile(file, FEATURES[feat]),
);
}
21 changes: 5 additions & 16 deletions packages/babel-helper-create-class-features-plugin/src/index.js
Expand Up @@ -15,14 +15,15 @@ import {
import { injectInitialization, extractComputedKeys } from "./misc";
import {
enableFeature,
enableFeatureIfCompilingFields,
verifyUsedFeatures,
FEATURES,
isLoose,
} from "./features";

import pkg from "../package.json";

export { FEATURES, injectInitialization };
export { FEATURES, enableFeatureIfCompilingFields, injectInitialization };

// Note: Versions are represented as an integer. e.g. 7.1.5 is represented
// as 70000100005. This method is easier than using a semver-parsing
Expand Down Expand Up @@ -53,7 +54,9 @@ export function createClassFeaturePlugin({
Class(path, state) {
if (this.file.get(versionKey) !== version) return;

verifyUsedFeatures(path, this.file);
if (!verifyUsedFeatures(path, this.file)) {
return;
}

const loose = isLoose(this.file, feature);

Expand All @@ -66,8 +69,6 @@ export function createClassFeaturePlugin({
const body = path.get("body");

for (const path of body.get("body")) {
verifyUsedFeatures(path, this.file);

if (path.node.computed) {
computedPaths.push(path);
}
Expand Down Expand Up @@ -120,18 +121,6 @@ export function createClassFeaturePlugin({
}

if (!isDecorated) isDecorated = hasOwnDecorators(path.node);

if (path.isStaticBlock?.()) {
throw path.buildCodeFrameError(`Incorrect plugin order, \`@babel/plugin-proposal-class-static-block\` should be placed before class features plugins
{
"plugins": [
"@babel/plugin-proposal-class-static-block",
"@babel/plugin-proposal-private-property-in-object",
"@babel/plugin-proposal-private-methods",
"@babel/plugin-proposal-class-properties",
]
}`);
}
}

if (!props.length && !isDecorated) return;
Expand Down
@@ -0,0 +1,5 @@
class B {
foo = 2;

#bar() {};
}
@@ -0,0 +1,8 @@
{
"plugins": [
["external-helpers", { "helperVersion": "7.100.0" }],
"syntax-class-properties",
"proposal-class-properties"
],
"throws": "Class private methods are not enabled.\nYou can enable @babel/plugin-proposal-private-methods to compile them."
}
@@ -0,0 +1,11 @@
class B {
#foo = 1;
bar = 2;
}

class A {
@deco
foo() {}

bar = 2;
}
@@ -0,0 +1,7 @@
{
"plugins": [
["external-helpers", { "helperVersion": "7.100.0" }],
["proposal-decorators", { "decoratorsBeforeExport": true }],
["syntax-class-properties"]
]
}
@@ -0,0 +1,33 @@
class B {
#foo = 1;
bar = 2;
}

let A = babelHelpers.decorate(null, function (_initialize) {
"use strict";

class A {
constructor() {
_initialize(this);
}

Comment on lines +1 to +13
Copy link
Member Author

Choose a reason for hiding this comment

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

Example of feature (1): the first class is not transformed.

}

return {
F: A,
d: [{
kind: "method",
decorators: [deco],
key: "foo",
value: function foo() {}
}, {
kind: "field",
key: "bar",

value() {
return 2;
}

}]
};
});
@@ -0,0 +1,8 @@
class A {
@deco
foo() {}
}

class B {
#priv;
}