UNPKG

@babel/plugin-transform-classes

Version:
519 lines (515 loc) 20.1 kB
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