@babel/plugin-transform-classes
Version:
Compile ES2015 classes to ES5
519 lines (515 loc) • 20.1 kB
JavaScript
import { declare } from '@babel/helper-plugin-utils';
import { isRequired } from '@babel/helper-compilation-targets';
import annotateAsPure from '@babel/helper-annotate-as-pure';
import { types, template } from '@babel/core';
import globalsBrowserUpper from '@babel/helper-globals/data/browser-upper.json' with { type: 'json' };
import globalsBuiltinUpper from '@babel/helper-globals/data/builtin-upper.json' with { type: 'json' };
import ReplaceSupers from '@babel/helper-replace-supers';
import { visitors } from '@babel/traverse';
function transformClass(path, file, builtinClasses, isLoose, assumptions, supportUnicodeId) {
const classState = {
classRef: undefined,
superName: null,
superReturns: [],
construct: undefined,
userConstructorPath: undefined,
body: [],
superThises: [],
pushedCreateClass: false,
protoAlias: null,
dynamicKeys: new Map(),
methods: {
instance: {
hasComputed: false,
list: [],
map: new Map()
},
static: {
hasComputed: false,
list: [],
map: new Map()
}
}
};
const scope = path.scope;
const findThisesVisitor = visitors.environmentVisitor({
ThisExpression(path) {
classState.superThises.push(path);
}
});
function createClassHelper(args) {
return types.callExpression(file.addHelper("createClass"), args);
}
function maybeCreateConstructor() {
const classBodyPath = path.get("body");
if (classBodyPath.node.body.some(path => types.isClassMethod(path) && path.kind === "constructor")) {
return;
}
const params = [];
let body;
if (classState.superName) {
body = template.statement.ast`{
super(...arguments);
}`;
} else {
body = types.blockStatement([]);
}
classBodyPath.unshiftContainer("body", types.classMethod("constructor", types.identifier("constructor"), params, body));
}
function pushBody() {
const classBodyPaths = path.get("body.body");
for (const path of classBodyPaths) {
const node = path.node;
if (path.isClassProperty() || path.isClassPrivateProperty()) {
throw path.buildCodeFrameError("Missing class properties transform.");
}
if (node.decorators) {
throw path.buildCodeFrameError("Method has decorators, put the decorator plugin before the classes one.");
}
if (types.isClassMethod(node)) {
const isConstructor = node.kind === "constructor";
const replaceSupers = new ReplaceSupers({
methodPath: path,
objectRef: classState.classRef,
superRef: classState.superName,
constantSuper: assumptions.constantSuper,
file: file,
refToPreserve: classState.classRef
});
replaceSupers.replace();
if (isConstructor) {
const superReturns = [];
path.traverse(visitors.environmentVisitor({
ReturnStatement(path) {
if (!path.getFunctionParent().isArrowFunctionExpression()) {
superReturns.push(path);
}
}
}));
classState.superReturns = superReturns;
classState.userConstructorPath = path;
types.inheritsComments(classState.construct, node).params = node.params;
} else {
path.ensureFunctionName(supportUnicodeId);
let wrapped;
if (node !== path.node) {
wrapped = path.node;
path.replaceWith(node);
}
pushMethod(node, wrapped);
}
}
}
}
function pushDescriptors() {
pushInheritsToBody();
const {
body
} = classState;
const props = {
instance: null,
static: null
};
for (const placement of ["static", "instance"]) {
if (classState.methods[placement].list.length) {
props[placement] = classState.methods[placement].list.map(desc => {
const obj = types.objectExpression([types.objectProperty(types.identifier("key"), desc.key)]);
for (const kind of ["get", "set", "value"]) {
if (desc[kind] != null) {
obj.properties.push(types.objectProperty(types.identifier(kind), desc[kind]));
}
}
return obj;
});
}
}
if (props.instance || props.static) {
let args = [types.cloneNode(classState.classRef), props.instance ? types.arrayExpression(props.instance) : types.nullLiteral(), props.static ? types.arrayExpression(props.static) : types.nullLiteral()];
let lastNonNullIndex = 0;
for (let i = 0; i < args.length; i++) {
if (!types.isNullLiteral(args[i])) lastNonNullIndex = i;
}
args = args.slice(0, lastNonNullIndex + 1);
body.push(types.returnStatement(createClassHelper(args)));
classState.pushedCreateClass = true;
}
}
function wrapSuperCall(bareSuper, superRef, thisRef, body) {
const bareSuperNode = bareSuper.node;
let call;
if (assumptions.superIsCallableConstructor) {
bareSuperNode.arguments.unshift(types.thisExpression());
if (bareSuperNode.arguments.length === 2 && types.isSpreadElement(bareSuperNode.arguments[1]) && types.isIdentifier(bareSuperNode.arguments[1].argument, {
name: "arguments"
})) {
bareSuperNode.arguments[1] = bareSuperNode.arguments[1].argument;
bareSuperNode.callee = types.memberExpression(types.cloneNode(superRef), types.identifier("apply"));
} else {
bareSuperNode.callee = types.memberExpression(types.cloneNode(superRef), types.identifier("call"));
}
call = types.logicalExpression("||", bareSuperNode, types.thisExpression());
} else {
const args = [types.thisExpression(), types.cloneNode(classState.classRef)];
if (bareSuperNode.arguments?.length) {
const bareSuperNodeArguments = bareSuperNode.arguments;
if (bareSuperNodeArguments.length === 1 && types.isSpreadElement(bareSuperNodeArguments[0]) && types.isIdentifier(bareSuperNodeArguments[0].argument, {
name: "arguments"
})) {
args.push(bareSuperNodeArguments[0].argument);
} else {
args.push(types.arrayExpression(bareSuperNodeArguments));
}
}
call = types.callExpression(file.addHelper("callSuper"), args);
}
if (bareSuper.parentPath.isExpressionStatement() && bareSuper.parentPath.container === body.node.body && body.node.body.length - 1 === bareSuper.parentPath.key) {
if (classState.superThises.length) {
call = types.assignmentExpression("=", thisRef(), call);
}
bareSuper.parentPath.replaceWith(types.returnStatement(call));
} else {
bareSuper.replaceWith(types.assignmentExpression("=", thisRef(), call));
}
}
function verifyConstructor() {
if (!classState.superName) return;
const path = classState.userConstructorPath;
const constructorBody = path.get("body");
let maxGuaranteedSuperBeforeIndex = constructorBody.node.body.length;
path.traverse(findThisesVisitor);
let thisRef = function () {
const ref = path.scope.generateDeclaredUidIdentifier("this");
maxGuaranteedSuperBeforeIndex++;
thisRef = () => types.cloneNode(ref);
return ref;
};
const buildAssertThisInitialized = function () {
return types.callExpression(file.addHelper("assertThisInitialized"), [thisRef()]);
};
const bareSupers = [];
path.traverse(visitors.environmentVisitor({
Super(path) {
const {
node,
parentPath
} = path;
if (parentPath.isCallExpression({
callee: node
})) {
bareSupers.unshift(parentPath);
}
}
}));
for (const bareSuper of bareSupers) {
wrapSuperCall(bareSuper, classState.superName, thisRef, constructorBody);
if (maxGuaranteedSuperBeforeIndex >= 0) {
let lastParentPath;
bareSuper.find(function (parentPath) {
if (parentPath === constructorBody) {
maxGuaranteedSuperBeforeIndex = Math.min(maxGuaranteedSuperBeforeIndex, lastParentPath.key);
return true;
}
const {
type
} = parentPath;
switch (type) {
case "ExpressionStatement":
case "SequenceExpression":
case "AssignmentExpression":
case "BinaryExpression":
case "MemberExpression":
case "CallExpression":
case "NewExpression":
case "VariableDeclarator":
case "VariableDeclaration":
case "BlockStatement":
case "ArrayExpression":
case "ObjectExpression":
case "ObjectProperty":
case "TemplateLiteral":
lastParentPath = parentPath;
return false;
default:
if (type === "LogicalExpression" && parentPath.node.left === lastParentPath.node || parentPath.isConditional() && parentPath.node.test === lastParentPath.node || type === "OptionalCallExpression" && parentPath.node.callee === lastParentPath.node || type === "OptionalMemberExpression" && parentPath.node.object === lastParentPath.node) {
lastParentPath = parentPath;
return false;
}
}
maxGuaranteedSuperBeforeIndex = -1;
return true;
});
}
}
const guaranteedCalls = new Set();
for (const thisPath of classState.superThises) {
const {
node,
parentPath
} = thisPath;
if (parentPath.isMemberExpression({
object: node
})) {
thisPath.replaceWith(thisRef());
continue;
}
let thisIndex;
thisPath.find(function (parentPath) {
if (parentPath.parentPath === constructorBody) {
thisIndex = parentPath.key;
return true;
}
return false;
});
let exprPath = thisPath.parentPath.isSequenceExpression() ? thisPath.parentPath : thisPath;
if (exprPath.listKey === "arguments" && (exprPath.parentPath.isCallExpression() || exprPath.parentPath.isOptionalCallExpression())) {
exprPath = exprPath.parentPath;
} else {
exprPath = null;
}
if (maxGuaranteedSuperBeforeIndex !== -1 && thisIndex > maxGuaranteedSuperBeforeIndex || guaranteedCalls.has(exprPath)) {
thisPath.replaceWith(thisRef());
} else {
if (exprPath) {
guaranteedCalls.add(exprPath);
}
thisPath.replaceWith(buildAssertThisInitialized());
}
}
let wrapReturn;
if (isLoose) {
wrapReturn = returnArg => {
const thisExpr = buildAssertThisInitialized();
return returnArg ? types.logicalExpression("||", returnArg, thisExpr) : thisExpr;
};
} else {
wrapReturn = returnArg => {
const returnParams = [thisRef()];
if (returnArg != null) {
returnParams.push(returnArg);
}
return types.callExpression(file.addHelper("possibleConstructorReturn"), returnParams);
};
}
const bodyPaths = constructorBody.get("body");
const guaranteedSuperBeforeFinish = maxGuaranteedSuperBeforeIndex !== -1 && maxGuaranteedSuperBeforeIndex < bodyPaths.length;
if (!bodyPaths.length || !bodyPaths.pop().isReturnStatement()) {
constructorBody.pushContainer("body", types.returnStatement(guaranteedSuperBeforeFinish ? thisRef() : buildAssertThisInitialized()));
}
for (const returnPath of classState.superReturns) {
returnPath.get("argument").replaceWith(wrapReturn(returnPath.node.argument));
}
}
function pushMethod(node, wrapped) {
if (node.kind === "method") {
if (processMethod(node)) return;
}
const placement = node.static ? "static" : "instance";
const methods = classState.methods[placement];
const descKey = node.kind === "method" ? "value" : node.kind;
const key = types.isNumericLiteral(node.key) || types.isBigIntLiteral(node.key) ? types.stringLiteral(String(node.key.value)) : types.toComputedKey(node);
methods.hasComputed = !types.isStringLiteral(key);
const fn = wrapped ?? types.toExpression(node);
let descriptor;
if (!methods.hasComputed && methods.map.has(key.value)) {
descriptor = methods.map.get(key.value);
descriptor[descKey] = fn;
if (descKey === "value") {
descriptor.get = null;
descriptor.set = null;
} else {
descriptor.value = null;
}
} else {
descriptor = {
key: key,
[descKey]: fn
};
methods.list.push(descriptor);
if (!methods.hasComputed) {
methods.map.set(key.value, descriptor);
}
}
}
function processMethod(node) {
if (assumptions.setClassMethods && !node.decorators) {
let {
classRef
} = classState;
if (!node.static) {
insertProtoAliasOnce();
classRef = classState.protoAlias;
}
const methodName = types.memberExpression(types.cloneNode(classRef), node.key, node.computed || types.isLiteral(node.key));
const func = types.functionExpression(node.id, node.params, node.body, node.generator, node.async);
types.inherits(func, node);
const expr = types.expressionStatement(types.assignmentExpression("=", methodName, func));
types.inheritsComments(expr, node);
classState.body.push(expr);
return true;
}
return false;
}
function insertProtoAliasOnce() {
if (classState.protoAlias === null) {
classState.protoAlias = scope.generateUidIdentifier("proto");
const classProto = types.memberExpression(classState.classRef, types.identifier("prototype"));
const protoDeclaration = types.variableDeclaration("var", [types.variableDeclarator(classState.protoAlias, classProto)]);
classState.body.push(protoDeclaration);
}
}
function pushInheritsToBody() {
if (!classState.superName) return;
classState.body.unshift(types.expressionStatement(types.callExpression(file.addHelper(isLoose ? "inheritsLoose" : "inherits"), [types.cloneNode(classState.classRef), types.cloneNode(classState.superName)])));
}
function extractDynamicKeys() {
const node = path.node;
const {
dynamicKeys
} = classState;
for (const elem of node.body.body) {
if (!types.isClassMethod(elem) || !elem.computed) continue;
if (scope.isPure(elem.key, true)) continue;
const id = scope.generateUidIdentifierBasedOnNode(elem.key);
dynamicKeys.set(id.name, elem.key);
elem.key = id;
}
}
function setupClosureParamsArgs() {
const superName = classState.superName;
const {
dynamicKeys
} = classState;
const closureParams = [];
const closureArgs = [];
if (superName) {
let arg = types.cloneNode(superName);
if (types.isIdentifier(superName) && builtinClasses.has(superName.name) && !scope.hasBinding(superName.name, true)) {
arg = types.callExpression(file.addHelper("wrapNativeSuper"), [arg]);
annotateAsPure(arg);
}
const param = scope.generateUidIdentifierBasedOnNode(superName);
closureParams.push(param);
closureArgs.push(arg);
classState.superName = types.cloneNode(param);
}
for (const [name, value] of dynamicKeys) {
closureParams.push(types.identifier(name));
closureArgs.push(value);
}
return {
closureParams,
closureArgs
};
}
function classTransformer(path) {
const node = path.node;
classState.superName = node.superClass;
classState.classRef = node.id ? types.identifier(node.id.name) : scope.generateUidIdentifier("class");
const {
classRef
} = classState;
const constructorBody = types.blockStatement([]);
classState.construct = types.inherits(types.functionDeclaration(types.cloneNode(classRef), [], constructorBody), node);
extractDynamicKeys();
const {
body
} = classState;
const {
closureParams,
closureArgs
} = setupClosureParamsArgs();
maybeCreateConstructor();
pushBody();
verifyConstructor();
pushDescriptors();
if (classState.userConstructorPath) {
const {
node
} = classState.userConstructorPath;
constructorBody.body.push(...node.body.body);
constructorBody.directives = node.body.directives;
types.inherits(classState.construct, node);
types.inherits(constructorBody, node.body);
}
if (!assumptions.noClassCalls) {
classState.construct.body.body.unshift(types.expressionStatement(types.callExpression(file.addHelper("classCallCheck"), [types.thisExpression(), types.cloneNode(classState.classRef)])));
}
const isStrict = path.isInStrictMode();
let constructorOnly = body.length === 0;
if (constructorOnly && !isStrict) {
constructorOnly = classState.construct.params.every(param => types.isIdentifier(param));
}
const directives = constructorOnly ? classState.construct.body.directives : [];
if (!isStrict) {
directives.push(types.directive(types.directiveLiteral("use strict")));
}
if (constructorOnly) {
const expr = types.toExpression(classState.construct);
return isLoose ? expr : createClassHelper([expr]);
}
if (!classState.pushedCreateClass) {
body.push(types.returnStatement(isLoose ? types.cloneNode(classState.classRef) : createClassHelper([types.cloneNode(classState.classRef)])));
}
body.unshift(classState.construct);
const container = types.arrowFunctionExpression(closureParams, types.blockStatement(body, directives));
return types.callExpression(container, closureArgs);
}
return classTransformer(path);
}
const builtinClasses = new Set([...globalsBrowserUpper, ...globalsBuiltinUpper]);
builtinClasses.delete("Iterator");
const index = declare((api, options) => {
api.assertVersion("^7.0.0-0 || ^8.0.0");
if ("loose" in options) {
console.warn("@babel/plugin-transform-classes: The 'loose' option has been deprecated, " + "use the 'setClassMethods', 'constantSuper', 'superIsCallableConstructor', and 'noClassCalls' assumptions instead (https://babeljs.io/assumptions).");
}
const {
loose = false
} = options;
const setClassMethods = api.assumption("setClassMethods") ?? loose;
const constantSuper = api.assumption("constantSuper") ?? loose;
const superIsCallableConstructor = api.assumption("superIsCallableConstructor") ?? loose;
const noClassCalls = api.assumption("noClassCalls") ?? loose;
const supportUnicodeId = !isRequired("transform-unicode-escapes", api.targets());
const VISITED = new WeakSet();
return {
name: "transform-classes",
visitor: {
ExportDefaultDeclaration(path) {
if (!path.get("declaration").isClassDeclaration()) return;
path.splitExportDeclaration();
},
ClassDeclaration(path) {
const {
node
} = path;
const ref = node.id ? types.cloneNode(node.id) : path.scope.generateUidIdentifier("class");
path.replaceWith(types.variableDeclaration("let", [types.variableDeclarator(ref, types.toExpression(node))]));
},
ClassExpression(path, state) {
const {
node
} = path;
if (VISITED.has(node)) return;
const replacement = path.ensureFunctionName(supportUnicodeId);
if (replacement && replacement.node !== node) return;
VISITED.add(node);
const [replacedPath] = path.replaceWith(transformClass(path, state.file, builtinClasses, loose, {
setClassMethods,
constantSuper,
superIsCallableConstructor,
noClassCalls
}, supportUnicodeId));
if (replacedPath.isCallExpression()) {
annotateAsPure(replacedPath);
const callee = replacedPath.get("callee");
if (callee.isArrowFunctionExpression()) {
callee.arrowFunctionToExpression();
}
}
}
}
};
});
export { index as default };
//# sourceMappingURL=index.js.map