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

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
},
};
}