From d1d3c823ccce9925cf144a2e55f4e66f4aff4634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Sun, 9 Dec 2018 12:30:25 +0100 Subject: [PATCH] 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 --- .../src/decorators.js | 155 +++++++++++- .../src/features.js | 20 +- .../src/fields.js | 29 ++- .../src/index.js | 103 +++++--- .../private/regression-T7364/output.mjs | 6 +- .../decorator-interop/output.js | 4 +- .../package.json | 3 +- .../src/index.js | 31 ++- .../src/transformer.js | 239 ------------------ 9 files changed, 284 insertions(+), 306 deletions(-) delete mode 100644 packages/babel-plugin-proposal-decorators/src/transformer.js diff --git a/packages/babel-helper-create-class-features-plugin/src/decorators.js b/packages/babel-helper-create-class-features-plugin/src/decorators.js index e0f66f2c6a51..4b0df1aef49c 100644 --- a/packages/babel-helper-create-class-features-plugin/src/decorators.js +++ b/packages/babel-helper-create-class-features-plugin/src/decorators.js @@ -1,3 +1,154 @@ -export function hasDecorators(path) { - return !!(path.node.decorators && path.node.decorators.length); +import { types as t, template } from "@babel/core"; +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); + }, + }; } diff --git a/packages/babel-helper-create-class-features-plugin/src/features.js b/packages/babel-helper-create-class-features-plugin/src/features.js index 49fb2059a592..afd8a29f2030 100644 --- a/packages/babel-helper-create-class-features-plugin/src/features.js +++ b/packages/babel-helper-create-class-features-plugin/src/features.js @@ -1,4 +1,4 @@ -import { hasDecorators } from "./decorators"; +import { hasOwnDecorators } from "./decorators"; export const FEATURES = Object.freeze({ //classes: 1 << 0, @@ -39,14 +39,18 @@ export function isLoose(file, feature) { } export function verifyUsedFeatures(path, file) { - if (hasDecorators(path) && !hasFeature(file, FEATURES.decorators)) { - throw path.buildCodeFrameError("Decorators are not enabled."); - } + if (hasOwnDecorators(path)) { + if (!hasFeature(file, FEATURES.decorators)) { + throw path.buildCodeFrameError("Decorators are not enabled."); + } - if (hasFeature(file, FEATURES.decorators)) { - throw new Error( - "@babel/plugin-class-features doesn't support decorators yet.", - ); + if (path.isPrivate()) { + throw path.buildCodeFrameError( + `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 diff --git a/packages/babel-helper-create-class-features-plugin/src/fields.js b/packages/babel-helper-create-class-features-plugin/src/fields.js index 9c8def9ec710..c1807bb132aa 100644 --- a/packages/babel-helper-create-class-features-plugin/src/fields.js +++ b/packages/babel-helper-create-class-features-plugin/src/fields.js @@ -318,6 +318,7 @@ export function buildFieldsInitNodes( ) { const staticNodes = []; const instanceNodes = []; + let needsClassRef = false; for (const prop of props) { const isStatic = prop.node.static; @@ -329,19 +330,23 @@ export function buildFieldsInitNodes( switch (true) { case isStatic && isPrivate && isField && loose: + needsClassRef = true; staticNodes.push( buildPrivateFieldInitLoose(t.cloneNode(ref), prop, privateNamesMap), ); break; case isStatic && isPrivate && isField && !loose: + needsClassRef = true; staticNodes.push( buildPrivateStaticFieldInitSpec(prop, privateNamesMap), ); break; case isStatic && isPublic && isField && loose: + needsClassRef = true; staticNodes.push(buildPublicFieldInitLoose(t.cloneNode(ref), prop)); break; case isStatic && isPublic && isField && !loose: + needsClassRef = true; staticNodes.push( 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; + }, + }; } diff --git a/packages/babel-helper-create-class-features-plugin/src/index.js b/packages/babel-helper-create-class-features-plugin/src/index.js index ac259a24c346..b4476a188c89 100644 --- a/packages/babel-helper-create-class-features-plugin/src/index.js +++ b/packages/babel-helper-create-class-features-plugin/src/index.js @@ -1,11 +1,16 @@ import nameFunction from "@babel/helper-function-name"; -import { types as t } from "@babel/core"; +import splitExportDeclaration from "@babel/helper-split-export-declaration"; import { buildPrivateNamesNodes, buildPrivateNamesMap, transformPrivateNamesUsage, buildFieldsInitNodes, } from "./fields"; +import { + hasOwnDecorators, + buildDecoratedClass, + hasDecorators, +} from "./decorators"; import { injectInitialization, extractComputedKeys } from "./misc"; import { enableFeature, @@ -54,7 +59,9 @@ export function createClassFeaturePlugin({ const loose = isLoose(this.file, FEATURES.fields); let constructor; + let isDecorated = hasOwnDecorators(path.node); const props = []; + const elements = []; const computedPaths = []; const privateNames = new Set(); const body = path.get("body"); @@ -75,14 +82,19 @@ export function createClassFeaturePlugin({ privateNames.add(name); } - if (path.isProperty() || path.isPrivate()) { - props.push(path); - } else if (path.isClassMethod({ kind: "constructor" })) { + if (path.isClassMethod({ kind: "constructor" })) { constructor = path; + } else { + elements.push(path); + if (path.isProperty() || path.isPrivate()) { + props.push(path); + } } + + if (!isDecorated) isDecorated = hasOwnDecorators(path.node); } - if (!props.length) return; + if (!props.length && !isDecorated) return; let ref; @@ -93,13 +105,9 @@ export function createClassFeaturePlugin({ ref = path.node.id; } - const keysNodes = extractComputedKeys( - ref, - path, - computedPaths, - this.file, - ); - + // NODE: These three functions don't support decorators yet, + // but verifyUsedFeatures throws if there are both + // decorators and private fields. const privateNamesMap = buildPrivateNamesMap(props); const privateNamesNodes = buildPrivateNamesNodes( privateNamesMap, @@ -109,19 +117,34 @@ export function createClassFeaturePlugin({ transformPrivateNamesUsage(ref, path, privateNamesMap, loose, state); - const { staticNodes, instanceNodes } = buildFieldsInitNodes( - ref, - props, - privateNamesMap, - state, - loose, - ); + 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, + props, + privateNamesMap, + state, + loose, + )); + } + if (instanceNodes.length > 0) { injectInitialization( path, constructor, instanceNodes, (referenceVisitor, state) => { + if (isDecorated) return; for (const prop of props) { if (prop.node.static) continue; prop.traverse(referenceVisitor, state); @@ -130,28 +153,7 @@ export function createClassFeaturePlugin({ ); } - 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 = wrapClass(path); path.insertBefore(keysNodes); path.insertAfter([...privateNamesNodes, ...staticNodes]); }, @@ -161,6 +163,25 @@ export function createClassFeaturePlugin({ 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"; + } + } + }, }, }; } diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/regression-T7364/output.mjs b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/regression-T7364/output.mjs index 7afab860194f..183240a1c2a0 100644 --- a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/regression-T7364/output.mjs +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/regression-T7364/output.mjs @@ -1,5 +1,3 @@ -var _class; - class MyClass { constructor() { var _this = this; @@ -22,7 +20,7 @@ class MyClass { var _myAsyncMethod = new WeakMap(); -_class = class MyClass2 { +(class MyClass2 { constructor() { var _this2 = this; @@ -40,7 +38,7 @@ _class = class MyClass2 { }); } -}; +}); var _myAsyncMethod2 = new WeakMap(); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/static-property-tdz/decorator-interop/output.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/static-property-tdz/decorator-interop/output.js index 70334680ced4..a615c9b9b3d6 100644 --- a/packages/babel-plugin-proposal-class-properties/test/fixtures/static-property-tdz/decorator-interop/output.js +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/static-property-tdz/decorator-interop/output.js @@ -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 }); } @@ -14,7 +14,7 @@ function _initializerWarningHelper(descriptor, context) { throw new Error('Decor function dec() {} -let A = (_class = (_temp = (_Symbol$search = Symbol.search, _class2 = +let A = (_class = (_temp = (_Symbol$search = Symbol.search, /*#__PURE__*/ function () { "use strict"; diff --git a/packages/babel-plugin-proposal-decorators/package.json b/packages/babel-plugin-proposal-decorators/package.json index 822a96642477..1e2bf31e2d61 100644 --- a/packages/babel-plugin-proposal-decorators/package.json +++ b/packages/babel-plugin-proposal-decorators/package.json @@ -16,8 +16,7 @@ ], "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.0.0", + "@babel/helper-create-class-features-plugin": "^7.2.1", "@babel/plugin-syntax-decorators": "^7.2.0" }, "peerDependencies": { diff --git a/packages/babel-plugin-proposal-decorators/src/index.js b/packages/babel-plugin-proposal-decorators/src/index.js index 1bcb52ae82a1..4d0e05cb57d2 100644 --- a/packages/babel-plugin-proposal-decorators/src/index.js +++ b/packages/babel-plugin-proposal-decorators/src/index.js @@ -1,6 +1,11 @@ +/* eslint-disable local-rules/plugin-name */ + import { declare } from "@babel/helper-plugin-utils"; 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"; export default declare((api, options) => { @@ -31,14 +36,26 @@ export default declare((api, options) => { } } - return { + if (legacy) { + return { + name: "proposal-decorators", + inherits: syntaxDecorators, + manipulateOptions({ generatorOpts }) { + generatorOpts.decoratorsBeforeExport = decoratorsBeforeExport; + }, + visitor: legacyVisitor, + }; + } + + return createClassFeaturePlugin({ name: "proposal-decorators", - inherits: syntaxDecorators, - manipulateOptions({ generatorOpts }) { + feature: FEATURES.decorators, + // loose: options.loose, Not supported + + manipulateOptions({ generatorOpts, parserOpts }) { + parserOpts.plugins.push(["decorators", { decoratorsBeforeExport }]); generatorOpts.decoratorsBeforeExport = decoratorsBeforeExport; }, - - visitor: legacy ? legacyVisitor : visitor, - }; + }); }); diff --git a/packages/babel-plugin-proposal-decorators/src/transformer.js b/packages/babel-plugin-proposal-decorators/src/transformer.js deleted file mode 100644 index 621538c74cd3..000000000000 --- a/packages/babel-plugin-proposal-decorators/src/transformer.js +++ /dev/null @@ -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)); - } - }, -};