Merge class features plugins

* Create @babel/plugin-class-features

* Move class properties transformation logic to enanced-classes (#8130)
This commit is contained in:
Nicolò Ribaudo 2018-11-20 21:14:35 +01:00 committed by GitHub
parent a2afb974be
commit 5979b0669b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 788 additions and 520 deletions

View File

@ -0,0 +1,3 @@
src
test
*.log

View File

@ -0,0 +1,19 @@
# @babel/plugin-class-features
> Compile class public and private fields, private methods and decorators to ES6
See our website [@babel/plugin-class-features](https://babeljs.io/docs/en/next/babel-plugin-class-features.html) for more information.
## Install
Using npm:
```sh
npm install --save-dev @babel/plugin-class-features
```
or using yarn:
```sh
yarn add @babel/plugin-class-features --dev
```

View File

@ -0,0 +1,27 @@
{
"name": "@babel/plugin-class-features",
"version": "7.1.4",
"author": "The Babel Team (https://babeljs.io/team)",
"license": "MIT",
"description": "Compile class public and private fields, private methods and decorators to ES6",
"repository": "https://github.com/babel/babel/tree/master/packages/babel-plugin-class-features",
"main": "lib/index.js",
"keywords": [
"babel",
"babel-plugin"
],
"dependencies": {
"@babel/helper-function-name": "^7.1.0",
"@babel/helper-member-expression-to-functions": "^7.0.0",
"@babel/helper-optimise-call-expression": "^7.0.0",
"@babel/helper-plugin-utils": "^7.0.0",
"@babel/helper-replace-supers": "^7.1.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
},
"devDependencies": {
"@babel/core": "^7.1.0",
"@babel/helper-plugin-test-runner": "^7.0.0"
}
}

View File

@ -0,0 +1,3 @@
export function hasDecorators(path) {
return !!(path.node.decorators && path.node.decorators.length);
}

View File

@ -0,0 +1,62 @@
import { hasDecorators } from "./decorators";
export const FEATURES = Object.freeze({
//classes: 1 << 0,
fields: 1 << 1,
privateMethods: 1 << 2,
decorators: 1 << 3,
});
// We can't use a symbol because this needs to always be the same, even if
// this package isn't deduped by npm. e.g.
// - node_modules/
// - @babel/plugin-class-features
// - @babel/plugin-proposal-decorators
// - node_modules
// - @babel-plugin-class-features
const featuresKey = "@babel/plugin-class-features/featuresKey";
const looseKey = "@babel/plugin-class-features/looseKey";
export function enableFeature(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)) {
file.set(featuresKey, file.get(featuresKey) | feature);
if (loose) file.set(looseKey, file.get(looseKey) | feature);
}
}
function hasFeature(file, feature) {
return !!(file.get(featuresKey) & feature);
}
export function isLoose(file, feature) {
return !!(file.get(looseKey) & feature);
}
export function verifyUsedFeatures(path, file) {
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 (hasDecorators(path) && !hasFeature(file, FEATURES.decorators)) {
throw path.buildCodeFrameError("Decorators are not enabled.");
}
if (path.isProperty()) {
if (!hasFeature(file, FEATURES.fields)) {
throw path.buildCodeFrameError("Class fields are not enabled.");
}
}
}

View File

@ -0,0 +1,322 @@
import { template, traverse, types as t } from "@babel/core";
import { environmentVisitor } from "@babel/helper-replace-supers";
import memberExpressionToFunctions from "@babel/helper-member-expression-to-functions";
import optimiseCall from "@babel/helper-optimise-call-expression";
export function buildPrivateNamesMap(props) {
const privateNamesMap = new Map();
for (const prop of props) {
if (prop.isPrivate()) {
const { name } = prop.node.key.id;
privateNamesMap.set(name, {
id: prop.scope.generateUidIdentifier(name),
static: !!prop.node.static,
});
}
}
return privateNamesMap;
}
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
// secret non-enumerable property. Hence, we also need to generate that
// key (using the classPrivateFieldLooseKey helper) in loose mode.
// 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.
if (loose) {
initNodes.push(
template.statement.ast`
var ${id} = ${state.addHelper("classPrivateFieldLooseKey")}("${name}")
`,
);
} else if (!isStatic) {
initNodes.push(template.statement.ast`var ${id} = new WeakMap();`);
}
}
return initNodes;
}
// 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 = {
PrivateName(path) {
const { privateNamesMap } = this;
const { node, parentPath } = path;
if (!parentPath.isMemberExpression({ property: node })) return;
if (!privateNamesMap.has(node.id.name)) return;
this.handle(parentPath);
},
Class(path) {
const { privateNamesMap } = this;
const body = path.get("body.body");
for (const prop of body) {
if (!prop.isClassPrivateProperty()) continue;
if (!privateNamesMap.has(prop.node.key.id.name)) continue;
// This class redeclares the private name.
// So, we can only evaluate the things in the outer scope.
path.traverse(privateNameInnerVisitor, this);
path.skip();
break;
}
},
};
// Traverses the outer portion of a class, without touching the class's inner
// scope, for private names.
const privateNameInnerVisitor = traverse.visitors.merge([
{
PrivateName: privateNameVisitor.PrivateName,
},
environmentVisitor,
]);
const privateNameHandlerSpec = {
memoise(member, count) {
const { scope } = member;
const { object } = member.node;
const memo = scope.maybeGenerateMemoised(object);
if (!memo) {
return;
}
this.memoiser.set(object, memo, count);
},
receiver(member) {
const { object } = member.node;
if (this.memoiser.has(object)) {
return t.cloneNode(this.memoiser.get(object));
}
return t.cloneNode(object);
},
get(member) {
const { classRef, privateNamesMap, file } = this;
const { name } = member.node.property.id;
const { id, static: isStatic } = privateNamesMap.get(name);
if (isStatic) {
return t.callExpression(
file.addHelper("classStaticPrivateFieldSpecGet"),
[this.receiver(member), t.cloneNode(classRef), t.cloneNode(id)],
);
} else {
return t.callExpression(file.addHelper("classPrivateFieldGet"), [
this.receiver(member),
t.cloneNode(id),
]);
}
},
set(member, value) {
const { classRef, privateNamesMap, file } = this;
const { name } = member.node.property.id;
const { id, static: isStatic } = privateNamesMap.get(name);
if (isStatic) {
return t.callExpression(
file.addHelper("classStaticPrivateFieldSpecSet"),
[this.receiver(member), t.cloneNode(classRef), t.cloneNode(id), value],
);
} else {
return t.callExpression(file.addHelper("classPrivateFieldSet"), [
this.receiver(member),
t.cloneNode(id),
value,
]);
}
},
call(member, args) {
// The first access (the get) should do the memo assignment.
this.memoise(member, 1);
return optimiseCall(this.get(member), this.receiver(member), args);
},
};
const privateNameHandlerLoose = {
handle(member) {
const { privateNamesMap, file } = this;
const { object } = member.node;
const { name } = member.node.property.id;
member.replaceWith(
template.expression`BASE(REF, PROP)[PROP]`({
BASE: file.addHelper("classPrivateFieldLooseBase"),
REF: object,
PROP: privateNamesMap.get(name).id,
}),
);
},
};
export function transformPrivateNamesUsage(
ref,
path,
privateNamesMap,
loose,
state,
) {
const body = path.get("body");
if (loose) {
body.traverse(privateNameVisitor, {
privateNamesMap,
file: state,
...privateNameHandlerLoose,
});
} else {
memberExpressionToFunctions(body, privateNameVisitor, {
privateNamesMap,
classRef: ref,
file: state,
...privateNameHandlerSpec,
});
}
}
function buildPrivateFieldInitLoose(ref, prop, privateNamesMap) {
const { id } = privateNamesMap.get(prop.node.key.id.name);
const value = prop.node.value || prop.scope.buildUndefinedNode();
return template.statement.ast`
Object.defineProperty(${ref}, ${id}, {
// configurable is false by default
// enumerable is false by default
writable: true,
value: ${value}
});
`;
}
function buildPrivateInstanceFieldInitSpec(ref, prop, privateNamesMap) {
const { id } = privateNamesMap.get(prop.node.key.id.name);
const value = prop.node.value || prop.scope.buildUndefinedNode();
return template.statement.ast`${id}.set(${ref}, {
// configurable is always false for private elements
// enumerable is always false for private elements
writable: true,
value: ${value},
})`;
}
function buildPrivateStaticFieldInitSpec(prop, privateNamesMap) {
const { id } = privateNamesMap.get(prop.node.key.id.name);
const value = prop.node.value || prop.scope.buildUndefinedNode();
return template.statement.ast`
var ${id} = {
// configurable is false by default
// enumerable is false by default
writable: true,
value: ${value}
};
`;
}
function buildPublicFieldInitLoose(ref, prop) {
const { key, computed } = prop.node;
const value = prop.node.value || prop.scope.buildUndefinedNode();
return t.expressionStatement(
t.assignmentExpression(
"=",
t.memberExpression(ref, key, computed || t.isLiteral(key)),
value,
),
);
}
function buildPublicFieldInitSpec(ref, prop, state) {
const { key, computed } = prop.node;
const value = prop.node.value || prop.scope.buildUndefinedNode();
return t.expressionStatement(
t.callExpression(state.addHelper("defineProperty"), [
ref,
computed || t.isLiteral(key) ? key : t.stringLiteral(key.name),
value,
]),
);
}
export function buildFieldsInitNodes(
ref,
props,
privateNamesMap,
state,
loose,
) {
const staticNodes = [];
const instanceNodes = [];
for (const prop of props) {
const isStatic = prop.node.static;
const isPrivate = prop.isPrivate();
// Pattern matching please
switch (true) {
case isStatic && isPrivate && loose:
staticNodes.push(
buildPrivateFieldInitLoose(t.cloneNode(ref), prop, privateNamesMap),
);
break;
case isStatic && isPrivate && !loose:
staticNodes.push(
buildPrivateStaticFieldInitSpec(prop, privateNamesMap),
);
break;
case isStatic && !isPrivate && loose:
staticNodes.push(buildPublicFieldInitLoose(t.cloneNode(ref), prop));
break;
case isStatic && !isPrivate && !loose:
staticNodes.push(
buildPublicFieldInitSpec(t.cloneNode(ref), prop, state),
);
break;
case !isStatic && isPrivate && loose:
instanceNodes.push(
buildPrivateFieldInitLoose(t.thisExpression(), prop, privateNamesMap),
);
break;
case !isStatic && isPrivate && !loose:
instanceNodes.push(
buildPrivateInstanceFieldInitSpec(
t.thisExpression(),
prop,
privateNamesMap,
),
);
break;
case !isStatic && !isPrivate && loose:
instanceNodes.push(buildPublicFieldInitLoose(t.thisExpression(), prop));
break;
case !isStatic && !isPrivate && !loose:
instanceNodes.push(
buildPublicFieldInitSpec(t.thisExpression(), prop, state),
);
break;
default:
throw new Error("Unreachable.");
}
}
return { staticNodes, instanceNodes };
}

View File

@ -0,0 +1,208 @@
import { declare } from "@babel/helper-plugin-utils";
import nameFunction from "@babel/helper-function-name";
import { types as t } from "@babel/core";
import {
buildPrivateNamesNodes,
buildPrivateNamesMap,
transformPrivateNamesUsage,
buildFieldsInitNodes,
} from "./fields";
import { injectInitialization, extractComputedKeys } from "./misc";
import {
enableFeature,
verifyUsedFeatures,
FEATURES,
setLoose,
isLoose,
} from "./features";
import pkg from "../package.json";
export { enableFeature, FEATURES, setLoose };
// 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
// package, but it breaks if we relese x.y.z where x, y or z are
// greater than 99_999.
const version = pkg.version.split(".").reduce((v, x) => v * 1e5 + +x, 0);
const versionKey = "@babel/plugin-class-features/version";
const getFeatureOptions = (options, name) => {
const value = options[name];
if (value === undefined || value === false) return { enabled: false };
if (value === true) return { enabled: true, loose: false };
if (typeof value === "object") {
if (
typeof value.loose !== "undefined" &&
typeof value.loose !== "boolean"
) {
throw new Error(`.${name}.loose must be a boolean or undefined.`);
}
return { enabled: true, loose: !!value.loose };
}
throw new Error(
`.${name} must be a boolean, an object with a 'loose'` +
` property or undefined.`,
);
};
export default declare((api, options) => {
api.assertVersion(7);
const fields = getFeatureOptions(options, "fields");
const privateMethods = getFeatureOptions(options, "privateMethods");
const decorators = getFeatureOptions(options, "decorators");
return {
name: "class-features",
manipulateOptions(opts, parserOpts) {
if (fields) {
parserOpts.plugins.push("classProperties", "classPrivateProperties");
}
},
pre() {
if (!this.file.get(versionKey) || this.file.get(versionKey) < version) {
this.file.set(versionKey, version);
}
if (fields.enabled) {
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) {
throw new Error("Decorators are not supported yet");
enableFeature(this.file, FEATURES.decorators);
}
},
visitor: {
Class(path, state) {
if (this.file.get(versionKey) !== version) return;
verifyUsedFeatures(path, this.file);
// Only fields are currently supported, this needs to be moved somewhere
// else when other features are added.
const loose = isLoose(this.file, FEATURES.fields);
let constructor;
const props = [];
const computedPaths = [];
const privateNames = new Set();
const body = path.get("body");
for (const path of body.get("body")) {
verifyUsedFeatures(path, this.file);
if (path.node.computed) {
computedPaths.push(path);
}
if (path.isClassPrivateProperty()) {
const { name } = path.node.key.id;
if (privateNames.has(name)) {
throw path.buildCodeFrameError("Duplicate private field");
}
privateNames.add(name);
}
if (path.isProperty()) {
props.push(path);
} else if (path.isClassMethod({ kind: "constructor" })) {
constructor = path;
}
}
if (!props.length) return;
let ref;
if (path.isClassExpression() || !path.node.id) {
nameFunction(path);
ref = path.scope.generateUidIdentifier("class");
} else {
// path.isClassDeclaration() && path.node.id
ref = path.node.id;
}
const keysNodes = extractComputedKeys(
ref,
path,
computedPaths,
this.file,
);
const privateNamesMap = buildPrivateNamesMap(props);
const privateNamesNodes = buildPrivateNamesNodes(
privateNamesMap,
loose,
state,
);
transformPrivateNamesUsage(ref, path, privateNamesMap, loose, state);
const { staticNodes, instanceNodes } = buildFieldsInitNodes(
ref,
props,
privateNamesMap,
state,
loose,
);
if (instanceNodes.length > 0) {
injectInitialization(
path,
constructor,
instanceNodes,
(referenceVisitor, state) => {
for (const prop of props) {
if (prop.node.static) continue;
prop.traverse(referenceVisitor, state);
}
},
);
}
for (const prop of props) {
prop.remove();
}
if (
keysNodes.length === 0 &&
staticNodes.length === 0 &&
privateNamesNodes.length === 0
) {
return;
}
if (path.isClassExpression()) {
path.scope.push({ id: ref });
path.replaceWith(
t.assignmentExpression("=", t.cloneNode(ref), path.node),
);
} else if (!path.node.id) {
// Anonymous class declaration
path.node.id = ref;
}
path.insertBefore(keysNodes);
path.insertAfter([...privateNamesNodes, ...staticNodes]);
},
PrivateName(path) {
if (this.file.get(versionKey) !== version) return;
throw path.buildCodeFrameError(`Unknown PrivateName "${path}"`);
},
},
};
});

View File

@ -0,0 +1,111 @@
import { template, traverse, types as t } from "@babel/core";
import { environmentVisitor } from "@babel/helper-replace-supers";
const findBareSupers = traverse.visitors.merge([
{
Super(path) {
const { node, parentPath } = path;
if (parentPath.isCallExpression({ callee: node })) {
this.push(parentPath);
}
},
},
environmentVisitor,
]);
const referenceVisitor = {
"TSTypeAnnotation|TypeAnnotation"(path) {
path.skip();
},
ReferencedIdentifier(path) {
if (this.scope.hasOwnBinding(path.node.name)) {
this.scope.rename(path.node.name);
path.skip();
}
},
};
const classFieldDefinitionEvaluationTDZVisitor = traverse.visitors.merge([
{
ReferencedIdentifier(path) {
if (
this.classBinding &&
this.classBinding === path.scope.getBinding(path.node.name)
) {
const classNameTDZError = this.file.addHelper("classNameTDZError");
const throwNode = t.callExpression(classNameTDZError, [
t.stringLiteral(path.node.name),
]);
path.replaceWith(t.sequenceExpression([throwNode, path.node]));
path.skip();
}
},
},
environmentVisitor,
]);
export function injectInitialization(path, constructor, nodes, renamer) {
if (!nodes.length) return;
const isDerived = !!path.node.superClass;
if (!constructor) {
const newConstructor = t.classMethod(
"constructor",
t.identifier("constructor"),
[],
t.blockStatement([]),
);
if (isDerived) {
newConstructor.params = [t.restElement(t.identifier("args"))];
newConstructor.body.body.push(template.statement.ast`super(...args)`);
}
[constructor] = path.get("body").unshiftContainer("body", newConstructor);
}
if (renamer) {
renamer(referenceVisitor, { scope: constructor.scope });
}
if (isDerived) {
const bareSupers = [];
constructor.traverse(findBareSupers, bareSupers);
for (const bareSuper of bareSupers) {
bareSuper.insertAfter(nodes);
}
} else {
constructor.get("body").unshiftContainer("body", nodes);
}
}
export function extractComputedKeys(ref, path, computedPaths, file) {
const declarations = [];
for (const computedPath of computedPaths) {
computedPath.traverse(classFieldDefinitionEvaluationTDZVisitor, {
classBinding: path.node.id && path.scope.getBinding(path.node.id.name),
file,
});
const computedNode = computedPath.node;
// Make sure computed property names are only evaluated once (upon class definition)
// and in the right order in combination with static properties
if (!computedPath.get("key").isConstantExpression()) {
const ident = path.scope.generateUidIdentifierBasedOnNode(
computedNode.key,
);
declarations.push(
t.variableDeclaration("var", [
t.variableDeclarator(ident, computedNode.key),
]),
);
computedNode.key = t.cloneNode(ident);
}
}
return declarations;
}

View File

@ -0,0 +1,6 @@
{
"plugins": [
["proposal-class-properties", { "loose": true }, "name 1"],
["proposal-class-properties", { "loose": false }, "name 2"]
]
}

View File

@ -0,0 +1,6 @@
class A {
constructor() {
this.foo = void 0;
}
}

View File

@ -0,0 +1,3 @@
import runner from "@babel/helper-plugin-test-runner";
runner(__dirname);

View File

@ -12,12 +12,8 @@
"babel-plugin"
],
"dependencies": {
"@babel/helper-function-name": "^7.1.0",
"@babel/helper-member-expression-to-functions": "^7.0.0",
"@babel/helper-optimise-call-expression": "^7.0.0",
"@babel/helper-plugin-utils": "^7.0.0",
"@babel/helper-replace-supers": "^7.1.0",
"@babel/plugin-syntax-class-properties": "^7.0.0"
"@babel/plugin-class-features": "^7.1.4"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"

View File

@ -1,525 +1,25 @@
import { declare } from "@babel/helper-plugin-utils";
import nameFunction from "@babel/helper-function-name";
import syntaxClassProperties from "@babel/plugin-syntax-class-properties";
import { template, traverse, types as t } from "@babel/core";
import { environmentVisitor } from "@babel/helper-replace-supers";
import memberExpressionToFunctions from "@babel/helper-member-expression-to-functions";
import optimiseCall from "@babel/helper-optimise-call-expression";
import pluginClassFeatures, {
enableFeature,
FEATURES,
} from "@babel/plugin-class-features";
export default declare((api, options) => {
api.assertVersion(7);
const { loose } = options;
const findBareSupers = traverse.visitors.merge([
{
Super(path) {
const { node, parentPath } = path;
if (parentPath.isCallExpression({ callee: node })) {
this.push(parentPath);
}
},
},
environmentVisitor,
]);
const referenceVisitor = {
"TSTypeAnnotation|TypeAnnotation"(path) {
path.skip();
},
ReferencedIdentifier(path) {
if (this.scope.hasOwnBinding(path.node.name)) {
this.scope.rename(path.node.name);
path.skip();
}
},
};
const classFieldDefinitionEvaluationTDZVisitor = traverse.visitors.merge([
{
ReferencedIdentifier(path) {
if (
this.classBinding &&
this.classBinding === path.scope.getBinding(path.node.name)
) {
const classNameTDZError = this.file.addHelper("classNameTDZError");
const throwNode = t.callExpression(classNameTDZError, [
t.stringLiteral(path.node.name),
]);
path.replaceWith(t.sequenceExpression([throwNode, path.node]));
path.skip();
}
},
},
environmentVisitor,
]);
// 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 = {
PrivateName(path) {
const { name } = this;
const { node, parentPath } = path;
if (!parentPath.isMemberExpression({ property: node })) return;
if (node.id.name !== name) return;
this.handle(parentPath);
},
Class(path) {
const { name } = this;
const body = path.get("body.body");
for (const prop of body) {
if (!prop.isClassPrivateProperty()) continue;
if (prop.node.key.id.name !== name) continue;
// This class redeclares the private name.
// So, we can only evaluate the things in the outer scope.
path.traverse(privateNameInnerVisitor, this);
path.skip();
break;
}
},
};
// Traverses the outer portion of a class, without touching the class's inner
// scope, for private names.
const privateNameInnerVisitor = traverse.visitors.merge([
{
PrivateName: privateNameVisitor.PrivateName,
},
environmentVisitor,
]);
const privateNameHandlerSpec = {
memoise(member, count) {
const { scope } = member;
const { object } = member.node;
const memo = scope.maybeGenerateMemoised(object);
if (!memo) {
return;
}
this.memoiser.set(object, memo, count);
},
receiver(member) {
const { object } = member.node;
if (this.memoiser.has(object)) {
return t.cloneNode(this.memoiser.get(object));
}
return t.cloneNode(object);
},
get(member) {
const { map, file } = this;
return t.callExpression(file.addHelper("classPrivateFieldGet"), [
this.receiver(member),
t.cloneNode(map),
]);
},
set(member, value) {
const { map, file } = this;
return t.callExpression(file.addHelper("classPrivateFieldSet"), [
this.receiver(member),
t.cloneNode(map),
value,
]);
},
call(member, args) {
// The first access (the get) should do the memo assignment.
this.memoise(member, 1);
return optimiseCall(this.get(member), this.receiver(member), args);
},
};
const privateNameHandlerLoose = {
handle(member) {
const { prop, file } = this;
const { object } = member.node;
member.replaceWith(
template.expression`BASE(REF, PROP)[PROP]`({
BASE: file.addHelper("classPrivateFieldLooseBase"),
REF: object,
PROP: prop,
}),
);
},
};
const staticPrivatePropertyHandlerSpec = {
...privateNameHandlerSpec,
get(member) {
const { file, privateId, classRef } = this;
return t.callExpression(
file.addHelper("classStaticPrivateFieldSpecGet"),
[this.receiver(member), t.cloneNode(classRef), t.cloneNode(privateId)],
);
},
set(member, value) {
const { file, privateId, classRef } = this;
return t.callExpression(
file.addHelper("classStaticPrivateFieldSpecSet"),
[
this.receiver(member),
t.cloneNode(classRef),
t.cloneNode(privateId),
value,
],
);
},
};
function buildClassPropertySpec(ref, path, state) {
const { scope } = path;
const { key, value, computed } = path.node;
return t.expressionStatement(
t.callExpression(state.addHelper("defineProperty"), [
ref,
computed || t.isLiteral(key) ? key : t.stringLiteral(key.name),
value || scope.buildUndefinedNode(),
]),
);
}
function buildClassPropertyLoose(ref, path) {
const { scope } = path;
const { key, value, computed } = path.node;
return t.expressionStatement(
t.assignmentExpression(
"=",
t.memberExpression(ref, key, computed || t.isLiteral(key)),
value || scope.buildUndefinedNode(),
),
);
}
function buildClassPrivatePropertySpec(ref, path, initNodes, state) {
const { parentPath, scope } = path;
const { name } = path.node.key.id;
const map = scope.generateUidIdentifier(name);
memberExpressionToFunctions(parentPath, privateNameVisitor, {
name,
map,
file: state,
...privateNameHandlerSpec,
});
initNodes.push(
template.statement`var MAP = new WeakMap();`({
MAP: map,
}),
);
// Must be late evaluated in case it references another private field.
return () =>
template.statement`
MAP.set(REF, {
// configurable is always false for private elements
// enumerable is always false for private elements
writable: true,
value: VALUE
});
`({
MAP: map,
REF: ref,
VALUE: path.node.value || scope.buildUndefinedNode(),
});
}
function buildClassPrivatePropertyLooseHelper(ref, path, state) {
const { parentPath, scope } = path;
const { name } = path.node.key.id;
const prop = scope.generateUidIdentifier(name);
parentPath.traverse(privateNameVisitor, {
name,
prop,
file: state,
...privateNameHandlerLoose,
});
return {
keyDecl: template.statement`var PROP = HELPER(NAME);`({
PROP: prop,
HELPER: state.addHelper("classPrivateFieldLooseKey"),
NAME: t.stringLiteral(name),
}),
// Must be late evaluated in case it references another private field.
buildInit: () =>
template.statement.ast`
Object.defineProperty(${ref}, ${prop}, {
// configurable is false by default
// enumerable is false by default
writable: true,
value: ${path.node.value || scope.buildUndefinedNode()}
});
`,
};
}
function buildClassInstancePrivatePropertyLoose(ref, path, initNodes, state) {
const { keyDecl, buildInit } = buildClassPrivatePropertyLooseHelper(
ref,
path,
state,
);
initNodes.push(keyDecl);
return buildInit;
}
function buildClassStaticPrivatePropertyLoose(ref, path, state) {
const { keyDecl, buildInit } = buildClassPrivatePropertyLooseHelper(
ref,
path,
state,
);
return [keyDecl, buildInit()];
}
function buildClassStaticPrivatePropertySpec(ref, path, state) {
const { parentPath, scope } = path;
const { name } = path.node.key.id;
const privateId = scope.generateUidIdentifier(name);
memberExpressionToFunctions(parentPath, privateNameVisitor, {
name,
privateId,
classRef: ref,
file: state,
...staticPrivatePropertyHandlerSpec,
});
return [
template.statement.ast`
var ${privateId} = {
// configurable is always false for private elements
// enumerable is always false for private elements
writable: true,
value: ${path.node.value || scope.buildUndefinedNode()}
}
`,
];
}
const buildClassProperty = loose
? buildClassPropertyLoose
: buildClassPropertySpec;
const buildClassPrivateProperty = loose
? buildClassInstancePrivatePropertyLoose
: buildClassPrivatePropertySpec;
const buildClassStaticPrivateProperty = loose
? buildClassStaticPrivatePropertyLoose
: buildClassStaticPrivatePropertySpec;
return {
name: "proposal-class-properties",
inherits: syntaxClassProperties,
visitor: {
Class(path, state) {
const isDerived = !!path.node.superClass;
let constructor;
const props = [];
const computedPaths = [];
const privateNames = new Set();
const body = path.get("body");
inherits: pluginClassFeatures,
for (const path of body.get("body")) {
const { computed, decorators } = path.node;
if (computed) {
computedPaths.push(path);
}
if (decorators && decorators.length > 0) {
throw path.buildCodeFrameError(
"Decorators transform is necessary.",
);
}
if (path.isClassPrivateProperty()) {
const {
key: {
id: { name },
},
} = path.node;
if (privateNames.has(name)) {
throw path.buildCodeFrameError("Duplicate private field");
}
privateNames.add(name);
}
if (path.isProperty()) {
props.push(path);
} else if (path.isClassMethod({ kind: "constructor" })) {
constructor = path;
}
}
if (!props.length) return;
let ref;
if (path.isClassExpression() || !path.node.id) {
nameFunction(path);
ref = path.scope.generateUidIdentifier("class");
} else {
// path.isClassDeclaration() && path.node.id
ref = path.node.id;
}
const computedNodes = [];
const staticNodes = [];
const instanceBody = [];
for (const computedPath of computedPaths) {
computedPath.traverse(classFieldDefinitionEvaluationTDZVisitor, {
classBinding:
path.node.id && path.scope.getBinding(path.node.id.name),
file: this.file,
});
const computedNode = computedPath.node;
// Make sure computed property names are only evaluated once (upon class definition)
// and in the right order in combination with static properties
if (!computedPath.get("key").isConstantExpression()) {
const ident = path.scope.generateUidIdentifierBasedOnNode(
computedNode.key,
);
computedNodes.push(
t.variableDeclaration("var", [
t.variableDeclarator(ident, computedNode.key),
]),
);
computedNode.key = t.cloneNode(ident);
}
}
// Transform private props before publics.
const privateMaps = [];
const privateMapInits = [];
for (const prop of props) {
if (prop.isPrivate() && !prop.node.static) {
const inits = [];
privateMapInits.push(inits);
privateMaps.push(
buildClassPrivateProperty(t.thisExpression(), prop, inits, state),
);
}
}
let p = 0;
for (const prop of props) {
if (prop.node.static) {
if (prop.isPrivate()) {
staticNodes.push(
...buildClassStaticPrivateProperty(
t.cloneNode(ref),
prop,
state,
),
);
} else {
staticNodes.push(
buildClassProperty(t.cloneNode(ref), prop, state),
);
}
} else if (prop.isPrivate()) {
instanceBody.push(privateMaps[p]());
staticNodes.push(...privateMapInits[p]);
p++;
} else {
instanceBody.push(
buildClassProperty(t.thisExpression(), prop, state),
);
}
}
if (instanceBody.length) {
if (!constructor) {
const newConstructor = t.classMethod(
"constructor",
t.identifier("constructor"),
[],
t.blockStatement([]),
);
if (isDerived) {
newConstructor.params = [t.restElement(t.identifier("args"))];
newConstructor.body.body.push(
t.expressionStatement(
t.callExpression(t.super(), [
t.spreadElement(t.identifier("args")),
]),
),
);
}
[constructor] = body.unshiftContainer("body", newConstructor);
}
const state = { scope: constructor.scope };
for (const prop of props) {
if (prop.node.static) continue;
prop.traverse(referenceVisitor, state);
}
//
if (isDerived) {
const bareSupers = [];
constructor.traverse(findBareSupers, bareSupers);
for (const bareSuper of bareSupers) {
bareSuper.insertAfter(instanceBody);
}
} else {
constructor.get("body").unshiftContainer("body", instanceBody);
}
}
for (const prop of props) {
prop.remove();
}
if (computedNodes.length === 0 && staticNodes.length === 0) return;
if (path.isClassExpression()) {
path.scope.push({ id: ref });
path.replaceWith(
t.assignmentExpression("=", t.cloneNode(ref), path.node),
);
} else if (!path.node.id) {
// Anonymous class declaration
path.node.id = ref;
}
path.insertBefore(computedNodes);
path.insertAfter(staticNodes);
manipulateOptions(opts, parserOpts) {
parserOpts.plugins.push("classProperties", "classPrivateProperties");
},
PrivateName(path) {
throw path.buildCodeFrameError(`Unknown PrivateName "${path}"`);
},
pre() {
enableFeature(this.file, FEATURES.fields, loose);
},
};
});

View File

@ -18,13 +18,12 @@ class Foo {
var _foo = babelHelpers.classPrivateFieldLooseKey("foo");
var _bar = babelHelpers.classPrivateFieldLooseKey("bar");
Object.defineProperty(Foo, _foo, {
writable: true,
value: "foo"
});
var _bar = babelHelpers.classPrivateFieldLooseKey("bar");
var f = new Foo();
expect("foo" in Foo).toBe(false);
expect("bar" in f).toBe(false);

View File

@ -16,9 +16,9 @@ class Foo {
}
var _bar = new WeakMap();
var _foo = {
writable: true,
value: "foo"
};
var _bar = new WeakMap();