Move decorators transform to @babel/helper-create-class-features-plugin (#9059)

* Move decorators to @babel/plugin-class-features

* Minor refactoring

* Use the new helper package
This commit is contained in:
Nicolò Ribaudo 2018-12-09 12:30:25 +01:00 committed by GitHub
parent 9b005dedfd
commit d1d3c823cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 285 additions and 307 deletions

View File

@ -1,3 +1,154 @@
export function hasDecorators(path) { import { types as t, template } from "@babel/core";
return !!(path.node.decorators && path.node.decorators.length); import ReplaceSupers from "@babel/helper-replace-supers";
export function hasOwnDecorators(node) {
return !!(node.decorators && node.decorators.length);
}
export function hasDecorators(node) {
return hasOwnDecorators(node) || node.body.body.some(hasOwnDecorators);
}
function prop(key, value) {
if (!value) return null;
return t.objectProperty(t.identifier(key), value);
}
function value(body, params = [], async, generator) {
const method = t.objectMethod("method", t.identifier("value"), params, body);
method.async = !!async;
method.generator = !!generator;
return method;
}
function takeDecorators(node) {
let result;
if (node.decorators && node.decorators.length > 0) {
result = t.arrayExpression(
node.decorators.map(decorator => decorator.expression),
);
}
node.decorators = undefined;
return result;
}
function getKey(node) {
if (node.computed) {
return node.key;
} else if (t.isIdentifier(node.key)) {
return t.stringLiteral(node.key.name);
} else {
return t.stringLiteral(String(node.key.value));
}
}
// NOTE: This function can be easily bound as .bind(file, classRef, superRef)
// to make it easier to use it in a loop.
function extractElementDescriptor(/* this: File, */ classRef, superRef, path) {
const { node, scope } = path;
const isMethod = path.isClassMethod();
if (path.isPrivate()) {
throw path.buildCodeFrameError(
`Private ${
isMethod ? "methods" : "fields"
} in decorated classes are not supported yet.`,
);
}
new ReplaceSupers(
{
methodPath: path,
methodNode: node,
objectRef: classRef,
isStatic: node.static,
superRef,
scope,
file: this,
},
true,
).replace();
const properties = [
prop("kind", t.stringLiteral(isMethod ? node.kind : "field")),
prop("decorators", takeDecorators(node)),
prop("static", node.static && t.booleanLiteral(true)),
prop("key", getKey(node)),
isMethod
? value(node.body, node.params, node.async, node.generator)
: node.value
? value(template.ast`{ return ${node.value} }`)
: prop("value", scope.buildUndefinedNode()),
].filter(Boolean);
path.remove();
return t.objectExpression(properties);
}
function addDecorateHelper(file) {
try {
return file.addHelper("decorate");
} catch (err) {
if (err.code === "BABEL_HELPER_UNKNOWN") {
err.message +=
"\n '@babel/plugin-transform-decorators' in non-legacy mode" +
" requires '@babel/core' version ^7.0.2 and you appear to be using" +
" an older version.";
}
throw err;
}
}
export function buildDecoratedClass(ref, path, elements, file) {
const { node, scope } = path;
const initializeId = scope.generateUidIdentifier("initialize");
const isDeclaration = node.id && path.isDeclaration();
const isStrict = path.isInStrictMode();
const { superClass } = node;
node.type = "ClassDeclaration";
if (!node.id) node.id = t.cloneNode(ref);
let superId;
if (superClass) {
superId = scope.generateUidIdentifierBasedOnNode(node.superClass, "super");
node.superClass = superId;
}
const classDecorators = takeDecorators(node);
const definitions = t.arrayExpression(
elements.map(extractElementDescriptor.bind(file, node.id, superId)),
);
let replacement = template.expression.ast`
${addDecorateHelper(file)}(
${classDecorators || t.nullLiteral()},
function (${initializeId}, ${superClass ? superId : null}) {
${node}
return { F: ${t.cloneNode(node.id)}, d: ${definitions} };
},
${superClass}
)
`;
let classPathDesc = "arguments.1.body.body.0";
if (!isStrict) {
replacement.arguments[1].body.directives.push(
t.directive(t.directiveLiteral("use strict")),
);
}
if (isDeclaration) {
replacement = template.ast`let ${ref} = ${replacement}`;
classPathDesc = "declarations.0.init." + classPathDesc;
}
return {
instanceNodes: [template.statement.ast`${initializeId}(this)`],
wrapClass(path) {
path.replaceWith(replacement);
return path.get(classPathDesc);
},
};
} }

View File

@ -1,4 +1,4 @@
import { hasDecorators } from "./decorators"; import { hasOwnDecorators } from "./decorators";
export const FEATURES = Object.freeze({ export const FEATURES = Object.freeze({
//classes: 1 << 0, //classes: 1 << 0,
@ -39,15 +39,19 @@ export function isLoose(file, feature) {
} }
export function verifyUsedFeatures(path, file) { export function verifyUsedFeatures(path, file) {
if (hasDecorators(path) && !hasFeature(file, FEATURES.decorators)) { if (hasOwnDecorators(path)) {
if (!hasFeature(file, FEATURES.decorators)) {
throw path.buildCodeFrameError("Decorators are not enabled."); throw path.buildCodeFrameError("Decorators are not enabled.");
} }
if (hasFeature(file, FEATURES.decorators)) { if (path.isPrivate()) {
throw new Error( throw path.buildCodeFrameError(
"@babel/plugin-class-features doesn't support decorators yet.", `Private ${
path.isClassMethod() ? "methods" : "fields"
} in decorated classes are not supported yet.`,
); );
} }
}
// NOTE: We can't use path.isPrivateMethod() because it isn't supported in <7.2.0 // NOTE: We can't use path.isPrivateMethod() because it isn't supported in <7.2.0
if (path.isPrivate() && path.isMethod()) { if (path.isPrivate() && path.isMethod()) {

View File

@ -318,6 +318,7 @@ export function buildFieldsInitNodes(
) { ) {
const staticNodes = []; const staticNodes = [];
const instanceNodes = []; const instanceNodes = [];
let needsClassRef = false;
for (const prop of props) { for (const prop of props) {
const isStatic = prop.node.static; const isStatic = prop.node.static;
@ -329,19 +330,23 @@ export function buildFieldsInitNodes(
switch (true) { switch (true) {
case isStatic && isPrivate && isField && loose: case isStatic && isPrivate && isField && loose:
needsClassRef = true;
staticNodes.push( staticNodes.push(
buildPrivateFieldInitLoose(t.cloneNode(ref), prop, privateNamesMap), buildPrivateFieldInitLoose(t.cloneNode(ref), prop, privateNamesMap),
); );
break; break;
case isStatic && isPrivate && isField && !loose: case isStatic && isPrivate && isField && !loose:
needsClassRef = true;
staticNodes.push( staticNodes.push(
buildPrivateStaticFieldInitSpec(prop, privateNamesMap), buildPrivateStaticFieldInitSpec(prop, privateNamesMap),
); );
break; break;
case isStatic && isPublic && isField && loose: case isStatic && isPublic && isField && loose:
needsClassRef = true;
staticNodes.push(buildPublicFieldInitLoose(t.cloneNode(ref), prop)); staticNodes.push(buildPublicFieldInitLoose(t.cloneNode(ref), prop));
break; break;
case isStatic && isPublic && isField && !loose: case isStatic && isPublic && isField && !loose:
needsClassRef = true;
staticNodes.push( staticNodes.push(
buildPublicFieldInitSpec(t.cloneNode(ref), prop, state), buildPublicFieldInitSpec(t.cloneNode(ref), prop, state),
); );
@ -397,5 +402,27 @@ export function buildFieldsInitNodes(
} }
} }
return { staticNodes, instanceNodes }; return {
staticNodes,
instanceNodes,
wrapClass(path) {
for (const prop of props) {
prop.remove();
}
if (!needsClassRef) return path;
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;
}
return path;
},
};
} }

View File

@ -1,11 +1,16 @@
import nameFunction from "@babel/helper-function-name"; import nameFunction from "@babel/helper-function-name";
import { types as t } from "@babel/core"; import splitExportDeclaration from "@babel/helper-split-export-declaration";
import { import {
buildPrivateNamesNodes, buildPrivateNamesNodes,
buildPrivateNamesMap, buildPrivateNamesMap,
transformPrivateNamesUsage, transformPrivateNamesUsage,
buildFieldsInitNodes, buildFieldsInitNodes,
} from "./fields"; } from "./fields";
import {
hasOwnDecorators,
buildDecoratedClass,
hasDecorators,
} from "./decorators";
import { injectInitialization, extractComputedKeys } from "./misc"; import { injectInitialization, extractComputedKeys } from "./misc";
import { import {
enableFeature, enableFeature,
@ -54,7 +59,9 @@ export function createClassFeaturePlugin({
const loose = isLoose(this.file, FEATURES.fields); const loose = isLoose(this.file, FEATURES.fields);
let constructor; let constructor;
let isDecorated = hasOwnDecorators(path.node);
const props = []; const props = [];
const elements = [];
const computedPaths = []; const computedPaths = [];
const privateNames = new Set(); const privateNames = new Set();
const body = path.get("body"); const body = path.get("body");
@ -75,14 +82,19 @@ export function createClassFeaturePlugin({
privateNames.add(name); privateNames.add(name);
} }
if (path.isClassMethod({ kind: "constructor" })) {
constructor = path;
} else {
elements.push(path);
if (path.isProperty() || path.isPrivate()) { if (path.isProperty() || path.isPrivate()) {
props.push(path); props.push(path);
} else if (path.isClassMethod({ kind: "constructor" })) {
constructor = path;
} }
} }
if (!props.length) return; if (!isDecorated) isDecorated = hasOwnDecorators(path.node);
}
if (!props.length && !isDecorated) return;
let ref; let ref;
@ -93,13 +105,9 @@ export function createClassFeaturePlugin({
ref = path.node.id; ref = path.node.id;
} }
const keysNodes = extractComputedKeys( // NODE: These three functions don't support decorators yet,
ref, // but verifyUsedFeatures throws if there are both
path, // decorators and private fields.
computedPaths,
this.file,
);
const privateNamesMap = buildPrivateNamesMap(props); const privateNamesMap = buildPrivateNamesMap(props);
const privateNamesNodes = buildPrivateNamesNodes( const privateNamesNodes = buildPrivateNamesNodes(
privateNamesMap, privateNamesMap,
@ -109,19 +117,34 @@ export function createClassFeaturePlugin({
transformPrivateNamesUsage(ref, path, privateNamesMap, loose, state); transformPrivateNamesUsage(ref, path, privateNamesMap, loose, state);
const { staticNodes, instanceNodes } = buildFieldsInitNodes( let keysNodes, staticNodes, instanceNodes, wrapClass;
if (isDecorated) {
staticNodes = keysNodes = [];
({ instanceNodes, wrapClass } = buildDecoratedClass(
ref,
path,
elements,
this.file,
));
} else {
keysNodes = extractComputedKeys(ref, path, computedPaths, this.file);
({ staticNodes, instanceNodes, wrapClass } = buildFieldsInitNodes(
ref, ref,
props, props,
privateNamesMap, privateNamesMap,
state, state,
loose, loose,
); ));
}
if (instanceNodes.length > 0) { if (instanceNodes.length > 0) {
injectInitialization( injectInitialization(
path, path,
constructor, constructor,
instanceNodes, instanceNodes,
(referenceVisitor, state) => { (referenceVisitor, state) => {
if (isDecorated) return;
for (const prop of props) { for (const prop of props) {
if (prop.node.static) continue; if (prop.node.static) continue;
prop.traverse(referenceVisitor, state); prop.traverse(referenceVisitor, state);
@ -130,28 +153,7 @@ export function createClassFeaturePlugin({
); );
} }
for (const prop of props) { path = wrapClass(path);
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.insertBefore(keysNodes);
path.insertAfter([...privateNamesNodes, ...staticNodes]); path.insertAfter([...privateNamesNodes, ...staticNodes]);
}, },
@ -161,6 +163,25 @@ export function createClassFeaturePlugin({
throw path.buildCodeFrameError(`Unknown PrivateName "${path}"`); throw path.buildCodeFrameError(`Unknown PrivateName "${path}"`);
}, },
ExportDefaultDeclaration(path) {
if (this.file.get(versionKey) !== version) return;
const decl = path.get("declaration");
if (decl.isClassDeclaration() && hasDecorators(decl.node)) {
if (decl.node.id) {
// export default class Foo {}
// -->
// class Foo {} export { Foo as default }
splitExportDeclaration(path);
} else {
// Annyms class declarations can be
// transformed as if they were expressions
decl.node.type = "ClassExpression";
}
}
},
}, },
}; };
} }

View File

@ -1,5 +1,3 @@
var _class;
class MyClass { class MyClass {
constructor() { constructor() {
var _this = this; var _this = this;
@ -22,7 +20,7 @@ class MyClass {
var _myAsyncMethod = new WeakMap(); var _myAsyncMethod = new WeakMap();
_class = class MyClass2 { (class MyClass2 {
constructor() { constructor() {
var _this2 = this; var _this2 = this;
@ -40,7 +38,7 @@ _class = class MyClass2 {
}); });
} }
}; });
var _myAsyncMethod2 = new WeakMap(); var _myAsyncMethod2 = new WeakMap();

View File

@ -1,4 +1,4 @@
var _class, _descriptor, _class2, _Symbol$search, _temp; var _class, _descriptor, _Symbol$search, _temp;
function _initializerDefineProperty(target, property, descriptor, context) { if (!descriptor) return; Object.defineProperty(target, property, { enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: descriptor.writable, value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 }); } function _initializerDefineProperty(target, property, descriptor, context) { if (!descriptor) return; Object.defineProperty(target, property, { enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: descriptor.writable, value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 }); }
@ -14,7 +14,7 @@ function _initializerWarningHelper(descriptor, context) { throw new Error('Decor
function dec() {} function dec() {}
let A = (_class = (_temp = (_Symbol$search = Symbol.search, _class2 = let A = (_class = (_temp = (_Symbol$search = Symbol.search,
/*#__PURE__*/ /*#__PURE__*/
function () { function () {
"use strict"; "use strict";

View File

@ -16,8 +16,7 @@
], ],
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0",
"@babel/helper-replace-supers": "^7.1.0", "@babel/helper-create-class-features-plugin": "^7.2.1",
"@babel/helper-split-export-declaration": "^7.0.0",
"@babel/plugin-syntax-decorators": "^7.2.0" "@babel/plugin-syntax-decorators": "^7.2.0"
}, },
"peerDependencies": { "peerDependencies": {

View File

@ -1,6 +1,11 @@
/* eslint-disable local-rules/plugin-name */
import { declare } from "@babel/helper-plugin-utils"; import { declare } from "@babel/helper-plugin-utils";
import syntaxDecorators from "@babel/plugin-syntax-decorators"; import syntaxDecorators from "@babel/plugin-syntax-decorators";
import visitor from "./transformer"; import {
createClassFeaturePlugin,
FEATURES,
} from "@babel/helper-create-class-features-plugin";
import legacyVisitor from "./transformer-legacy"; import legacyVisitor from "./transformer-legacy";
export default declare((api, options) => { export default declare((api, options) => {
@ -31,14 +36,26 @@ export default declare((api, options) => {
} }
} }
if (legacy) {
return { return {
name: "proposal-decorators", name: "proposal-decorators",
inherits: syntaxDecorators, inherits: syntaxDecorators,
manipulateOptions({ generatorOpts }) { manipulateOptions({ generatorOpts }) {
generatorOpts.decoratorsBeforeExport = decoratorsBeforeExport; generatorOpts.decoratorsBeforeExport = decoratorsBeforeExport;
}, },
visitor: legacyVisitor,
visitor: legacy ? legacyVisitor : visitor,
}; };
}
return createClassFeaturePlugin({
name: "proposal-decorators",
feature: FEATURES.decorators,
// loose: options.loose, Not supported
manipulateOptions({ generatorOpts, parserOpts }) {
parserOpts.plugins.push(["decorators", { decoratorsBeforeExport }]);
generatorOpts.decoratorsBeforeExport = decoratorsBeforeExport;
},
});
}); });

View File

@ -1,239 +0,0 @@
import { types as t, template } from "@babel/core";
import splitExportDeclaration from "@babel/helper-split-export-declaration";
import ReplaceSupers from "@babel/helper-replace-supers";
function prop(key, value) {
if (!value) return null;
return t.objectProperty(t.identifier(key), value);
}
function value(body, params = [], async, generator) {
const method = t.objectMethod("method", t.identifier("value"), params, body);
method.async = !!async;
method.generator = !!generator;
return method;
}
function hasDecorators({ node }) {
if (node.decorators && node.decorators.length > 0) return true;
const body = node.body.body;
for (let i = 0; i < body.length; i++) {
const method = body[i];
if (method.decorators && method.decorators.length > 0) {
return true;
}
}
return false;
}
function takeDecorators({ node }) {
let result;
if (node.decorators && node.decorators.length > 0) {
result = t.arrayExpression(
node.decorators.map(decorator => decorator.expression),
);
}
node.decorators = undefined;
return result;
}
function getKey(node) {
if (node.computed) {
return node.key;
} else if (t.isIdentifier(node.key)) {
return t.stringLiteral(node.key.name);
} else {
return t.stringLiteral(String(node.key.value));
}
}
function getSingleElementDefinition(path, superRef, classRef, file) {
const { node, scope } = path;
const isMethod = path.isClassMethod();
if (path.isPrivate()) {
throw path.buildCodeFrameError(
`Private ${
isMethod ? "methods" : "fields"
} in decorated classes are not supported yet.`,
);
}
new ReplaceSupers(
{
methodPath: path,
methodNode: node,
objectRef: classRef,
isStatic: node.static,
superRef,
scope,
file,
},
true,
).replace();
const properties = [
prop("kind", t.stringLiteral(isMethod ? node.kind : "field")),
prop("decorators", takeDecorators(path)),
prop("static", node.static && t.booleanLiteral(true)),
prop("key", getKey(node)),
isMethod
? value(node.body, node.params, node.async, node.generator)
: node.value
? value(template.ast`{ return ${node.value} }`)
: prop("value", scope.buildUndefinedNode()),
].filter(Boolean);
return t.objectExpression(properties);
}
function getElementsDefinitions(path, fId, file) {
const elements = [];
for (const p of path.get("body.body")) {
if (!p.isClassMethod({ kind: "constructor" })) {
elements.push(
getSingleElementDefinition(p, path.node.superClass, fId, file),
);
p.remove();
}
}
return t.arrayExpression(elements);
}
function getConstructorPath(path) {
return path
.get("body.body")
.find(path => path.isClassMethod({ kind: "constructor" }));
}
const bareSupersVisitor = {
CallExpression(path, { initializeInstanceElements }) {
if (path.get("callee").isSuper()) {
path.insertAfter(t.cloneNode(initializeInstanceElements));
// Sometimes this path gets requeued (e.g. in (super(), foo)), and
// it leads to infinite recursion.
path.skip();
}
},
Function(path) {
if (!path.isArrowFunctionExpression()) path.skip();
},
};
function insertInitializeInstanceElements(path, initializeInstanceId) {
const isBase = !path.node.superClass;
const initializeInstanceElements = t.callExpression(initializeInstanceId, [
t.thisExpression(),
]);
const constructorPath = getConstructorPath(path);
if (constructorPath) {
if (isBase) {
constructorPath
.get("body")
.unshiftContainer("body", [
t.expressionStatement(initializeInstanceElements),
]);
} else {
constructorPath.traverse(bareSupersVisitor, {
initializeInstanceElements,
});
}
} else {
const constructor = isBase
? t.classMethod(
"constructor",
t.identifier("constructor"),
[],
t.blockStatement([t.expressionStatement(initializeInstanceElements)]),
)
: t.classMethod(
"constructor",
t.identifier("constructor"),
[t.restElement(t.identifier("args"))],
t.blockStatement([
t.expressionStatement(
t.callExpression(t.Super(), [
t.spreadElement(t.identifier("args")),
]),
),
t.expressionStatement(initializeInstanceElements),
]),
);
path.node.body.body.push(constructor);
}
}
function transformClass(path, file) {
const isDeclaration = path.node.id && path.isDeclaration();
const isStrict = path.isInStrictMode();
const { superClass } = path.node;
path.node.type = "ClassDeclaration";
if (!path.node.id) path.node.id = path.scope.generateUidIdentifier("class");
const initializeId = path.scope.generateUidIdentifier("initialize");
const superId =
superClass &&
path.scope.generateUidIdentifierBasedOnNode(path.node.superClass, "super");
if (superClass) path.node.superClass = superId;
const classDecorators = takeDecorators(path);
const definitions = getElementsDefinitions(path, path.node.id, file);
insertInitializeInstanceElements(path, initializeId);
const expr = template.expression.ast`
${addDecorateHelper(file)}(
${classDecorators || t.nullLiteral()},
function (${initializeId}, ${superClass ? superId : null}) {
${path.node}
return { F: ${t.cloneNode(path.node.id)}, d: ${definitions} };
},
${superClass}
)
`;
if (!isStrict) {
expr.arguments[1].body.directives.push(
t.directive(t.directiveLiteral("use strict")),
);
}
return isDeclaration ? template.ast`let ${path.node.id} = ${expr}` : expr;
}
function addDecorateHelper(file) {
try {
return file.addHelper("decorate");
} catch (err) {
if (err.code === "BABEL_HELPER_UNKNOWN") {
err.message +=
"\n '@babel/plugin-transform-decorators' in non-legacy mode" +
" requires '@babel/core' version ^7.0.2 and you appear to be using" +
" an older version.";
}
throw err;
}
}
export default {
ExportDefaultDeclaration(path) {
let decl = path.get("declaration");
if (!decl.isClassDeclaration() || !hasDecorators(decl)) return;
if (decl.node.id) decl = splitExportDeclaration(path);
decl.replaceWith(transformClass(decl, this.file));
},
Class(path) {
if (hasDecorators(path)) {
path.replaceWith(transformClass(path, this.file));
}
},
};