Skip to content

Commit

Permalink
Move decorators transform to @babel/helper-create-class-features-plug…
Browse files Browse the repository at this point in the history
…in (#9059)

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

* Minor refactoring

* Use the new helper package
  • Loading branch information
nicolo-ribaudo committed Dec 9, 2018
1 parent 9b005de commit d1d3c82
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 306 deletions.
155 changes: 153 additions & 2 deletions 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);
},
};
}
20 changes: 12 additions & 8 deletions 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,
Expand Down Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion packages/babel-helper-create-class-features-plugin/src/fields.js
Expand Up @@ -318,6 +318,7 @@ export function buildFieldsInitNodes(
) {
const staticNodes = [];
const instanceNodes = [];
let needsClassRef = false;

for (const prop of props) {
const isStatic = prop.node.static;
Expand All @@ -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),
);
Expand Down Expand Up @@ -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;
},
};
}

0 comments on commit d1d3c82

Please sign in to comment.