UNPKG

@babel/helper-create-class-features-plugin

Version:

Compile class public and private fields, private methods and decorators to ES6

1,375 lines (1,371 loc) 97.8 kB
import { types, template } from '@babel/core'; import semver from 'semver'; import { visitors } from '@babel/traverse'; import ReplaceSupers from '@babel/helper-replace-supers'; import memberExpressionToFunctions from '@babel/helper-member-expression-to-functions'; import optimiseCall from '@babel/helper-optimise-call-expression'; import annotateAsPure from '@babel/helper-annotate-as-pure'; import { skipTransparentExprWrapperNodes, skipTransparentExprWrappers } from '@babel/helper-skip-transparent-expression-wrappers'; function assertFieldTransformed(path) { if (path.node.declare || path.isClassProperty({ definite: true })) { throw path.buildCodeFrameError(`TypeScript 'declare' fields must first be transformed by ` + `@babel/plugin-transform-typescript.\n` + `If you have already enabled that plugin (or '@babel/preset-typescript'), make sure ` + `that it runs before any plugin related to additional class features:\n` + ` - @babel/plugin-transform-class-properties\n` + ` - @babel/plugin-transform-private-methods\n` + ` - @babel/plugin-proposal-decorators`); } } function buildPrivateNamesMap(className, privateFieldsAsSymbolsOrProperties, props) { const privateNamesMap = new Map(); let classBrandId; for (const prop of props) { if (prop.isPrivate()) { const { name } = prop.node.key.id; let update = privateNamesMap.get(name); if (!update) { const isMethod = !prop.isProperty(); const isStatic = prop.node.static; let initAdded = false; let id; if (!privateFieldsAsSymbolsOrProperties && isMethod && !isStatic) { initAdded = !!classBrandId; classBrandId ??= prop.scope.generateUidIdentifier(`${className}_brand`); id = classBrandId; } else { id = prop.scope.generateUidIdentifier(name); } update = { id, static: isStatic, method: isMethod, initAdded }; privateNamesMap.set(name, update); } if (prop.isClassPrivateMethod()) { if (prop.node.kind === "get") { const { body } = prop.node.body; let $; if (body.length === 1 && types.isReturnStatement($ = body[0]) && types.isCallExpression($ = $.argument) && $.arguments.length === 1 && types.isThisExpression($.arguments[0]) && types.isIdentifier($ = $.callee)) { update.getId = types.cloneNode($); update.getterDeclared = true; } else { update.getId = prop.scope.generateUidIdentifier(`get_${name}`); } } else if (prop.node.kind === "set") { const { params } = prop.node; const { body } = prop.node.body; let $; if (body.length === 1 && types.isExpressionStatement($ = body[0]) && types.isCallExpression($ = $.expression) && $.arguments.length === 2 && types.isThisExpression($.arguments[0]) && types.isIdentifier($.arguments[1], { name: params[0].name }) && types.isIdentifier($ = $.callee)) { update.setId = types.cloneNode($); update.setterDeclared = true; } else { update.setId = prop.scope.generateUidIdentifier(`set_${name}`); } } else if (prop.node.kind === "method") { update.methodId = prop.scope.generateUidIdentifier(name); } } privateNamesMap.set(name, update); } } return privateNamesMap; } function buildPrivateNamesNodes(privateNamesMap, privateFieldsAsProperties, privateFieldsAsSymbols, state) { const initNodes = []; const injectedIds = new Set(); for (const [name, value] of privateNamesMap) { const { static: isStatic, method: isMethod } = value; const id = types.cloneNode(value.id); let init; if (privateFieldsAsProperties) { init = types.callExpression(state.addHelper("classPrivateFieldLooseKey"), [types.stringLiteral(name)]); } else if (privateFieldsAsSymbols) { init = types.callExpression(types.identifier("Symbol"), [types.stringLiteral(name)]); } else if (!isStatic) { if (injectedIds.has(id.name)) continue; injectedIds.add(id.name); init = types.newExpression(types.identifier(isMethod ? "WeakSet" : "WeakMap"), []); } if (init) { if (!privateFieldsAsSymbols) { annotateAsPure(init); } initNodes.push(template.statement.ast`var ${id} = ${init}`); } } return initNodes; } function privateNameVisitorFactory(visitor) { const nestedVisitor = visitors.environmentVisitor({ ...visitor }); const privateNameVisitor = { ...visitor, Class(path) { const { privateNamesMap } = this; const body = path.get("body.body"); const visiblePrivateNames = new Map(privateNamesMap); const redeclared = []; for (const prop of body) { if (!prop.isPrivate()) continue; const { name } = prop.node.key.id; visiblePrivateNames.delete(name); redeclared.push(name); } if (!redeclared.length) { return; } path.get("body").traverse(nestedVisitor, { ...this, redeclared }); path.traverse(privateNameVisitor, { ...this, privateNamesMap: visiblePrivateNames }); path.skipKey("body"); } }; return privateNameVisitor; } const privateNameVisitor = privateNameVisitorFactory({ PrivateName(path, { noDocumentAll }) { const { privateNamesMap, redeclared } = this; const { node, parentPath } = path; if (!parentPath.isMemberExpression({ property: node }) && !parentPath.isOptionalMemberExpression({ property: node })) { return; } const { name } = node.id; if (!privateNamesMap.has(name)) return; if (redeclared?.includes(name)) return; this.handle(parentPath, noDocumentAll); } }); function unshadow(name, scope, innerBinding) { while (scope?.hasBinding(name) && scope.getBindingIdentifier(name) !== innerBinding) { scope.rename(name); scope = scope.parent; } } function buildCheckInRHS(rhs, file, inRHSIsObject) { if (inRHSIsObject || !file.availableHelper?.("checkInRHS")) return rhs; return types.callExpression(file.addHelper("checkInRHS"), [rhs]); } const privateInVisitor = privateNameVisitorFactory({ BinaryExpression(path, { file }) { const { operator, left, right } = path.node; if (operator !== "in") return; if (!types.isPrivateName(left)) return; const { privateFieldsAsProperties, privateNamesMap, redeclared } = this; const { name } = left.id; if (!privateNamesMap.has(name)) return; if (redeclared?.includes(name)) return; unshadow(this.classRef.name, path.scope, this.innerBinding); if (privateFieldsAsProperties) { const { id } = privateNamesMap.get(name); path.replaceWith(template.expression.ast` Object.prototype.hasOwnProperty.call(${buildCheckInRHS(right, file)}, ${types.cloneNode(id)}) `); return; } const { id, static: isStatic } = privateNamesMap.get(name); if (isStatic) { path.replaceWith(template.expression.ast`${buildCheckInRHS(right, file)} === ${types.cloneNode(this.classRef)}`); return; } path.replaceWith(template.expression.ast`${types.cloneNode(id)}.has(${buildCheckInRHS(right, file)})`); } }); function readOnlyError(file, name) { return types.callExpression(file.addHelper("readOnlyError"), [types.stringLiteral(`#${name}`)]); } function writeOnlyError(file, name) { return types.callExpression(file.addHelper("writeOnlyError"), [types.stringLiteral(`#${name}`)]); } function buildStaticPrivateFieldAccess(expr, noUninitializedPrivateFieldAccess) { if (noUninitializedPrivateFieldAccess) return expr; return types.memberExpression(expr, types.identifier("_")); } function autoInherits(fn) { return function (member) { return types.inherits(fn.apply(this, arguments), member.node); }; } const privateNameHandlerSpec = { memoise(member, count) { const { scope } = member; const { object } = member.node; const memo = scope.maybeGenerateMemoised(object); if (!memo) { return; } this.memoiser.set(object, memo, count); }, receiver(member) { const { object } = member.node; if (this.memoiser.has(object)) { return types.cloneNode(this.memoiser.get(object)); } return types.cloneNode(object); }, get: autoInherits(function (member) { const { classRef, privateNamesMap, file, innerBinding, noUninitializedPrivateFieldAccess } = this; const privateName = member.node.property; const { name } = privateName.id; const { id, static: isStatic, method: isMethod, methodId, getId, setId } = privateNamesMap.get(name); const isGetterOrSetter = getId || setId; const cloneId = id => types.inherits(types.cloneNode(id), privateName); if (isStatic) { unshadow(classRef.name, member.scope, innerBinding); const receiver = this.receiver(member); const skipCheck = types.isIdentifier(receiver) && receiver.name === classRef.name; if (!isMethod) { if (skipCheck) { return buildStaticPrivateFieldAccess(cloneId(id), noUninitializedPrivateFieldAccess); } return buildStaticPrivateFieldAccess(types.callExpression(file.addHelper("assertClassBrand"), [types.cloneNode(classRef), receiver, cloneId(id)]), noUninitializedPrivateFieldAccess); } if (getId) { if (skipCheck) { return types.callExpression(cloneId(getId), [receiver]); } return types.callExpression(file.addHelper("classPrivateGetter"), [types.cloneNode(classRef), receiver, cloneId(getId)]); } if (setId) { const err = types.buildUndefinedNode(); if (skipCheck) return err; return types.sequenceExpression([types.callExpression(file.addHelper("assertClassBrand"), [types.cloneNode(classRef), receiver]), err]); } if (skipCheck) return cloneId(id); return types.callExpression(file.addHelper("assertClassBrand"), [types.cloneNode(classRef), receiver, cloneId(id)]); } if (isMethod) { if (isGetterOrSetter) { if (!getId) { return types.sequenceExpression([this.receiver(member), writeOnlyError(file, name)]); } return types.callExpression(file.addHelper("classPrivateGetter"), [types.cloneNode(id), this.receiver(member), cloneId(getId)]); } return types.callExpression(file.addHelper("assertClassBrand"), [types.cloneNode(id), this.receiver(member), cloneId(methodId)]); } return types.callExpression(file.addHelper("classPrivateFieldGet2"), [cloneId(id), this.receiver(member)]); }), boundGet(member) { this.memoise(member, 1); return types.callExpression(types.memberExpression(this.get(member), types.identifier("bind")), [this.receiver(member)]); }, set: autoInherits(function (member, value) { const { classRef, privateNamesMap, file, noUninitializedPrivateFieldAccess } = this; const privateName = member.node.property; const { name } = privateName.id; const { id, static: isStatic, method: isMethod, setId } = privateNamesMap.get(name); const cloneId = id => types.inherits(types.cloneNode(id), privateName); if (isStatic) { const receiver = this.receiver(member); const skipCheck = types.isIdentifier(receiver) && receiver.name === classRef.name; if (isMethod && !setId) { const err = readOnlyError(file, name); if (skipCheck) return types.sequenceExpression([value, err]); return types.sequenceExpression([value, types.callExpression(file.addHelper("assertClassBrand"), [types.cloneNode(classRef), receiver]), readOnlyError(file, name)]); } if (setId) { if (skipCheck) { return types.callExpression(types.cloneNode(setId), [receiver, value]); } return types.callExpression(file.addHelper("classPrivateSetter"), [types.cloneNode(classRef), cloneId(setId), receiver, value]); } return types.assignmentExpression("=", buildStaticPrivateFieldAccess(cloneId(id), noUninitializedPrivateFieldAccess), skipCheck ? value : types.callExpression(file.addHelper("assertClassBrand"), [types.cloneNode(classRef), receiver, value])); } if (isMethod) { if (setId) { return types.callExpression(file.addHelper("classPrivateSetter"), [types.cloneNode(id), cloneId(setId), this.receiver(member), value]); } return types.sequenceExpression([this.receiver(member), value, readOnlyError(file, name)]); } return types.callExpression(file.addHelper("classPrivateFieldSet2"), [cloneId(id), this.receiver(member), value]); }), destructureSet(member) { const { privateNamesMap, file, noUninitializedPrivateFieldAccess } = this; const privateName = member.node.property; const { name } = privateName.id; const { static: isStatic, method: isMethod, setId } = privateNamesMap.get(name); if (isMethod && !setId) { return types.memberExpression(types.sequenceExpression([member.node.object, readOnlyError(file, name)]), types.identifier("_")); } if (isStatic && !isMethod) { const getCall = this.get(member); if (!noUninitializedPrivateFieldAccess || !types.isCallExpression(getCall)) { return getCall; } const ref = getCall.arguments.pop(); getCall.arguments.push(template.expression.ast`(_) => ${ref} = _`); return types.memberExpression(types.callExpression(file.addHelper("toSetter"), [getCall]), types.identifier("_")); } const setCall = this.set(member, types.identifier("_")); if (!types.isCallExpression(setCall) || !types.isIdentifier(setCall.arguments[setCall.arguments.length - 1], { name: "_" })) { throw member.buildCodeFrameError("Internal Babel error while compiling this code. This is a Babel bug. " + "Please report it at https://github.com/babel/babel/issues."); } let args; if (types.isMemberExpression(setCall.callee, { computed: false }) && types.isIdentifier(setCall.callee.property) && setCall.callee.property.name === "call") { args = [setCall.callee.object, types.arrayExpression(setCall.arguments.slice(1, -1)), setCall.arguments[0]]; } else { args = [setCall.callee, types.arrayExpression(setCall.arguments.slice(0, -1))]; } return types.memberExpression(types.callExpression(file.addHelper("toSetter"), args), types.identifier("_")); }, call(member, args) { this.memoise(member, 1); return optimiseCall(this.get(member), this.receiver(member), args, false); }, optionalCall(member, args) { this.memoise(member, 1); return optimiseCall(this.get(member), this.receiver(member), args, true); }, delete() { throw new Error("Internal Babel error: deleting private elements is a parsing error."); } }; const privateNameHandlerLoose = { get(member) { const { privateNamesMap, file } = this; const { object } = member.node; const { name } = member.node.property.id; return template.expression`BASE(REF, PROP)[PROP]`({ BASE: file.addHelper("classPrivateFieldLooseBase"), REF: types.cloneNode(object), PROP: types.cloneNode(privateNamesMap.get(name).id) }); }, set() { throw new Error("private name handler with loose = true don't need set()"); }, boundGet(member) { return types.callExpression(types.memberExpression(this.get(member), types.identifier("bind")), [types.cloneNode(member.node.object)]); }, simpleSet(member) { return this.get(member); }, destructureSet(member) { return this.get(member); }, call(member, args) { return types.callExpression(this.get(member), args); }, optionalCall(member, args) { return types.optionalCallExpression(this.get(member), args, true); }, delete() { throw new Error("Internal Babel error: deleting private elements is a parsing error."); } }; function transformPrivateNamesUsage(ref, path, privateNamesMap, { privateFieldsAsProperties, noUninitializedPrivateFieldAccess, noDocumentAll, innerBinding }, state) { if (!privateNamesMap.size) return; const body = path.get("body"); const handler = privateFieldsAsProperties ? privateNameHandlerLoose : privateNameHandlerSpec; memberExpressionToFunctions(body, privateNameVisitor, { privateNamesMap, classRef: ref, file: state, ...handler, noDocumentAll, noUninitializedPrivateFieldAccess, innerBinding }); body.traverse(privateInVisitor, { privateNamesMap, classRef: ref, file: state, privateFieldsAsProperties, innerBinding }); } function buildPrivateFieldInitLoose(ref, prop, privateNamesMap) { const { id } = privateNamesMap.get(prop.node.key.id.name); const value = prop.node.value || types.buildUndefinedNode(); return inheritPropComments(template.statement.ast` Object.defineProperty(${ref}, ${types.cloneNode(id)}, { // configurable is false by default // enumerable is false by default writable: true, value: ${value} }); `, prop); } function buildPrivateInstanceFieldInitSpec(ref, prop, privateNamesMap, state) { const { id } = privateNamesMap.get(prop.node.key.id.name); const value = prop.node.value || types.buildUndefinedNode(); const helper = state.addHelper("classPrivateFieldInitSpec"); return inheritLoc(inheritPropComments(types.expressionStatement(types.callExpression(helper, [types.thisExpression(), inheritLoc(types.cloneNode(id), prop.node.key), value])), prop), prop.node); } function buildPrivateStaticFieldInitSpec(prop, privateNamesMap, noUninitializedPrivateFieldAccess) { const privateName = privateNamesMap.get(prop.node.key.id.name); const value = noUninitializedPrivateFieldAccess ? prop.node.value : template.expression.ast`{ _: ${prop.node.value || types.buildUndefinedNode()} }`; return inheritPropComments(types.variableDeclaration("var", [types.variableDeclarator(types.cloneNode(privateName.id), value)]), prop); } function buildPrivateMethodInitLoose(ref, prop, privateNamesMap) { const privateName = privateNamesMap.get(prop.node.key.id.name); const { methodId, id, getId, setId, initAdded } = privateName; if (initAdded) return; if (methodId) { return inheritPropComments(template.statement.ast` Object.defineProperty(${ref}, ${id}, { // configurable is false by default // enumerable is false by default // writable is false by default value: ${methodId.name} }); `, prop); } const isGetterOrSetter = getId || setId; if (isGetterOrSetter) { privateNamesMap.set(prop.node.key.id.name, { ...privateName, initAdded: true }); return inheritPropComments(template.statement.ast` Object.defineProperty(${ref}, ${id}, { // configurable is false by default // enumerable is false by default // writable is false by default get: ${getId ? getId.name : types.buildUndefinedNode()}, set: ${setId ? setId.name : types.buildUndefinedNode()} }); `, prop); } } function buildPrivateInstanceMethodInitSpec(ref, prop, privateNamesMap, state) { const privateName = privateNamesMap.get(prop.node.key.id.name); if (privateName.initAdded) return; return buildPrivateInstanceMethodInitialization(ref, prop, privateNamesMap, state); } function buildPrivateInstanceMethodInitialization(ref, prop, privateNamesMap, state) { const privateName = privateNamesMap.get(prop.node.key.id.name); const { id } = privateName; const helper = state.addHelper("classPrivateMethodInitSpec"); return inheritPropComments(template.statement.ast`${helper}( ${types.thisExpression()}, ${types.cloneNode(id)} )`, prop); } function buildPublicFieldInitLoose(ref, prop) { const { key, computed } = prop.node; const value = prop.node.value || types.buildUndefinedNode(); return inheritPropComments(types.expressionStatement(types.assignmentExpression("=", types.memberExpression(ref, key, computed || types.isLiteral(key)), value)), prop); } function buildPublicFieldInitSpec(ref, prop, state) { const { key, computed } = prop.node; const value = prop.node.value || types.buildUndefinedNode(); return inheritPropComments(types.expressionStatement(types.callExpression(state.addHelper("defineProperty"), [ref, computed || types.isLiteral(key) ? key : types.stringLiteral(key.name), value])), prop); } function buildPrivateStaticMethodInitLoose(ref, prop, privateNamesMap) { const privateName = privateNamesMap.get(prop.node.key.id.name); const { id, methodId, getId, setId, initAdded } = privateName; if (initAdded) return; const isGetterOrSetter = getId || setId; if (isGetterOrSetter) { privateNamesMap.set(prop.node.key.id.name, { ...privateName, initAdded: true }); return inheritPropComments(template.statement.ast` Object.defineProperty(${ref}, ${id}, { // configurable is false by default // enumerable is false by default // writable is false by default get: ${getId ? getId.name : types.buildUndefinedNode()}, set: ${setId ? setId.name : types.buildUndefinedNode()} }) `, prop); } return inheritPropComments(template.statement.ast` Object.defineProperty(${ref}, ${id}, { // configurable is false by default // enumerable is false by default // writable is false by default value: ${methodId.name} }); `, prop); } function buildPrivateMethodDeclaration(prop, privateNamesMap, privateFieldsAsSymbolsOrProperties = false) { const privateName = privateNamesMap.get(prop.node.key.id.name); const { id, methodId, getId, setId, getterDeclared, setterDeclared, static: isStatic } = privateName; const { params, body, generator, async } = prop.node; const isGetter = getId && params.length === 0; const isSetter = setId && params.length > 0; if (isGetter && getterDeclared || isSetter && setterDeclared) { privateNamesMap.set(prop.node.key.id.name, { ...privateName, initAdded: true }); return null; } if ((isGetter || isSetter) && !privateFieldsAsSymbolsOrProperties) { const scope = prop.get("body").scope; const thisArg = scope.generateUidIdentifier("this"); const state = { thisRef: thisArg, argumentsPath: [] }; prop.traverse(thisContextVisitor, state); if (state.argumentsPath.length) { const argumentsId = scope.generateUidIdentifier("arguments"); scope.push({ id: argumentsId, init: template.expression.ast`[].slice.call(arguments, 1)` }); for (const path of state.argumentsPath) { path.replaceWith(types.cloneNode(argumentsId)); } } params.unshift(types.cloneNode(thisArg)); } let declId = methodId; if (isGetter) { privateNamesMap.set(prop.node.key.id.name, { ...privateName, getterDeclared: true, initAdded: true }); declId = getId; } else if (isSetter) { privateNamesMap.set(prop.node.key.id.name, { ...privateName, setterDeclared: true, initAdded: true }); declId = setId; } else if (isStatic && !privateFieldsAsSymbolsOrProperties) { declId = id; } return inheritPropComments(types.functionDeclaration(types.cloneNode(declId), params, body, generator, async), prop); } const thisContextVisitor = visitors.environmentVisitor({ Identifier(path, state) { if (state.argumentsPath && path.node.name === "arguments") { state.argumentsPath.push(path); } }, UnaryExpression(path) { const { node } = path; if (node.operator === "delete") { const argument = skipTransparentExprWrapperNodes(node.argument); if (types.isThisExpression(argument)) { path.replaceWith(types.booleanLiteral(true)); } } }, ThisExpression(path, state) { state.needsClassRef = true; path.replaceWith(types.cloneNode(state.thisRef)); }, MetaProperty(path) { const { node } = path; if (node.meta.name === "new" && node.property.name === "target") { path.replaceWith(types.buildUndefinedNode()); } } }); const innerReferencesVisitor = { ReferencedIdentifier(path, state) { if (path.scope.getBindingIdentifier(path.node.name) === state.innerBinding) { state.needsClassRef = true; path.node.name = state.thisRef.name; } } }; function replaceThisContext(path, ref, innerBindingRef) { const state = { thisRef: ref, needsClassRef: false, innerBinding: innerBindingRef }; if (!path.isMethod()) { path.traverse(thisContextVisitor, state); } if (innerBindingRef != null && state.thisRef?.name && state.thisRef.name !== innerBindingRef.name) { path.traverse(innerReferencesVisitor, state); } return state.needsClassRef; } function isNameOrLength({ key, computed }) { if (key.type === "Identifier") { return !computed && (key.name === "name" || key.name === "length"); } if (key.type === "StringLiteral") { return key.value === "name" || key.value === "length"; } return false; } function inheritPropComments(node, prop) { types.inheritLeadingComments(node, prop.node); types.inheritInnerComments(node, prop.node); return node; } function inheritLoc(node, original) { node.start = original.start; node.end = original.end; node.loc = original.loc; return node; } function buildFieldsInitNodes(ref, superRef, props, privateNamesMap, file, setPublicClassFields, privateFieldsAsSymbolsOrProperties, noUninitializedPrivateFieldAccess, constantSuper, innerBindingRef) { let classRefFlags = 0; let injectSuperRef; const staticNodes = []; const instanceNodes = []; let lastInstanceNodeReturnsThis = false; const pureStaticNodes = []; let classBindingNode = null; const getSuperRef = types.isIdentifier(superRef) ? () => superRef : () => { injectSuperRef ??= props[0].scope.generateUidIdentifierBasedOnNode(superRef); return injectSuperRef; }; const classRefForInnerBinding = ref ?? props[0].scope.generateUidIdentifier(innerBindingRef.name || "Class"); ref ??= types.cloneNode(innerBindingRef); for (const prop of props) { if (prop.isClassProperty()) { assertFieldTransformed(prop); } const isStatic = !types.isStaticBlock(prop.node) && prop.node.static; const isInstance = !isStatic; const isPrivate = prop.isPrivate(); const isPublic = !isPrivate; const isField = prop.isProperty(); const isMethod = !isField; const isStaticBlock = prop.isStaticBlock(); if (isStatic) classRefFlags |= 1; if (isStatic || isMethod && isPrivate || isStaticBlock) { new ReplaceSupers({ methodPath: prop, constantSuper, file: file, refToPreserve: innerBindingRef, getSuperRef, getObjectRef() { classRefFlags |= 2; if (isStatic || isStaticBlock) { return classRefForInnerBinding; } else { return types.memberExpression(classRefForInnerBinding, types.identifier("prototype")); } } }).replace(); const replaced = replaceThisContext(prop, classRefForInnerBinding, innerBindingRef); if (replaced) { classRefFlags |= 2; } } lastInstanceNodeReturnsThis = false; switch (true) { case isStaticBlock: { const blockBody = prop.node.body; if (blockBody.length === 1 && types.isExpressionStatement(blockBody[0])) { staticNodes.push(inheritPropComments(blockBody[0], prop)); } else { staticNodes.push(types.inheritsComments(template.statement.ast`(() => { ${blockBody} })()`, prop.node)); } break; } case isStatic && isPrivate && isField && privateFieldsAsSymbolsOrProperties: staticNodes.push(buildPrivateFieldInitLoose(types.cloneNode(ref), prop, privateNamesMap)); break; case isStatic && isPrivate && isField && !privateFieldsAsSymbolsOrProperties: staticNodes.push(buildPrivateStaticFieldInitSpec(prop, privateNamesMap, noUninitializedPrivateFieldAccess)); break; case isStatic && isPublic && isField && setPublicClassFields: if (!isNameOrLength(prop.node)) { staticNodes.push(buildPublicFieldInitLoose(types.cloneNode(ref), prop)); break; } case isStatic && isPublic && isField && !setPublicClassFields: staticNodes.push(buildPublicFieldInitSpec(types.cloneNode(ref), prop, file)); break; case isInstance && isPrivate && isField && privateFieldsAsSymbolsOrProperties: instanceNodes.push(buildPrivateFieldInitLoose(types.thisExpression(), prop, privateNamesMap)); break; case isInstance && isPrivate && isField && !privateFieldsAsSymbolsOrProperties: instanceNodes.push(buildPrivateInstanceFieldInitSpec(types.thisExpression(), prop, privateNamesMap, file)); break; case isInstance && isPrivate && isMethod && privateFieldsAsSymbolsOrProperties: instanceNodes.unshift(buildPrivateMethodInitLoose(types.thisExpression(), prop, privateNamesMap)); pureStaticNodes.push(buildPrivateMethodDeclaration(prop, privateNamesMap, privateFieldsAsSymbolsOrProperties)); break; case isInstance && isPrivate && isMethod && !privateFieldsAsSymbolsOrProperties: instanceNodes.unshift(buildPrivateInstanceMethodInitSpec(types.thisExpression(), prop, privateNamesMap, file)); pureStaticNodes.push(buildPrivateMethodDeclaration(prop, privateNamesMap, privateFieldsAsSymbolsOrProperties)); break; case isStatic && isPrivate && isMethod && !privateFieldsAsSymbolsOrProperties: pureStaticNodes.push(buildPrivateMethodDeclaration(prop, privateNamesMap, privateFieldsAsSymbolsOrProperties)); break; case isStatic && isPrivate && isMethod && privateFieldsAsSymbolsOrProperties: staticNodes.unshift(buildPrivateStaticMethodInitLoose(types.cloneNode(ref), prop, privateNamesMap)); pureStaticNodes.push(buildPrivateMethodDeclaration(prop, privateNamesMap, privateFieldsAsSymbolsOrProperties)); break; case isInstance && isPublic && isField && setPublicClassFields: instanceNodes.push(buildPublicFieldInitLoose(types.thisExpression(), prop)); break; case isInstance && isPublic && isField && !setPublicClassFields: lastInstanceNodeReturnsThis = true; instanceNodes.push(buildPublicFieldInitSpec(types.thisExpression(), prop, file)); break; default: throw new Error("Unreachable."); } } if (classRefFlags & 2 && innerBindingRef != null) { classBindingNode = types.expressionStatement(types.assignmentExpression("=", types.cloneNode(classRefForInnerBinding), types.cloneNode(innerBindingRef))); } return { staticNodes: staticNodes.filter(Boolean), instanceNodes: instanceNodes.filter(Boolean), lastInstanceNodeReturnsThis, pureStaticNodes: pureStaticNodes.filter(Boolean), classBindingNode, wrapClass(path) { for (const prop of props) { prop.node.leadingComments = null; prop.remove(); } if (injectSuperRef) { path.scope.push({ id: types.cloneNode(injectSuperRef) }); path.set("superClass", types.assignmentExpression("=", injectSuperRef, path.node.superClass)); } if (classRefFlags !== 0) { if (path.isClassExpression()) { path.scope.push({ id: ref }); path.replaceWith(types.assignmentExpression("=", types.cloneNode(ref), path.node)); } else { if (innerBindingRef == null) { path.node.id = ref; } if (classBindingNode != null) { path.scope.push({ id: classRefForInnerBinding }); } } } return path; } }; } const findBareSupers = visitors.environmentVisitor({ Super(path) { const { node, parentPath } = path; if (parentPath.isCallExpression({ callee: node })) { this.push(parentPath); } } }); const referenceVisitor = { "TSTypeAnnotation|TypeAnnotation"(path) { path.skip(); }, ReferencedIdentifier(path, { scope }) { if (scope.hasOwnBinding(path.node.name)) { scope.rename(path.node.name); path.skip(); } } }; function handleClassTDZ(path, state) { if (state.classBinding && state.classBinding === path.scope.getBinding(path.node.name)) { const classNameTDZError = state.file.addHelper("classNameTDZError"); const throwNode = types.callExpression(classNameTDZError, [types.stringLiteral(path.node.name)]); path.replaceWith(types.sequenceExpression([throwNode, path.node])); path.skip(); } } const classFieldDefinitionEvaluationTDZVisitor = { ReferencedIdentifier: handleClassTDZ, "TSTypeAnnotation|TypeAnnotation"(path) { path.skip(); } }; function injectInitialization(path, constructor, nodes, renamer, lastReturnsThis) { if (!nodes.length) return; const isDerived = !!path.node.superClass; if (!constructor) { const newConstructor = types.classMethod("constructor", types.identifier("constructor"), [], types.blockStatement([])); if (isDerived) { newConstructor.params = [types.restElement(types.identifier("args"))]; newConstructor.body.body.push(template.statement.ast`super(...args)`); } [constructor] = path.get("body").unshiftContainer("body", newConstructor); } if (renamer) { renamer(referenceVisitor, { scope: constructor.scope }); } if (isDerived) { const bareSupers = []; constructor.traverse(findBareSupers, bareSupers); let isFirst = true; for (const bareSuper of bareSupers) { if (isFirst) { isFirst = false; } else { nodes = nodes.map(n => types.cloneNode(n)); } if (!bareSuper.parentPath.isExpressionStatement()) { const allNodes = [bareSuper.node, ...nodes.map(n => types.toExpression(n))]; if (!lastReturnsThis) allNodes.push(types.thisExpression()); bareSuper.replaceWith(types.sequenceExpression(allNodes)); } else { bareSuper.insertAfter(nodes); } } } else { constructor.get("body").unshiftContainer("body", nodes); } } function memoiseComputedKey(keyNode, scope, hint) { const isUidReference = types.isIdentifier(keyNode) && scope.hasUid(keyNode.name); if (isUidReference) { return; } const isMemoiseAssignment = types.isAssignmentExpression(keyNode, { operator: "=" }) && types.isIdentifier(keyNode.left) && scope.hasUid(keyNode.left.name); if (isMemoiseAssignment) { return types.cloneNode(keyNode); } else { const ident = types.identifier(hint); scope.push({ id: ident, kind: "let" }); return types.assignmentExpression("=", types.cloneNode(ident), keyNode); } } function extractComputedKeys(path, computedPaths, file) { const { scope } = path; const declarations = []; const state = { classBinding: path.node.id && scope.getBinding(path.node.id.name), file }; for (const computedPath of computedPaths) { const computedKey = computedPath.get("key"); if (computedKey.isReferencedIdentifier()) { handleClassTDZ(computedKey, state); } else { computedKey.traverse(classFieldDefinitionEvaluationTDZVisitor, state); } const computedNode = computedPath.node; if (!computedKey.isConstantExpression()) { const assignment = memoiseComputedKey(computedKey.node, scope, scope.generateUidBasedOnNode(computedKey.node)); if (assignment) { declarations.push(types.expressionStatement(assignment)); computedNode.key = types.cloneNode(assignment.left); } } } return declarations; } function hasOwnDecorators(node) { return !!node.decorators?.length; } function incrementId(id, idx = id.length - 1) { if (idx === -1) { id.unshift(65); return; } const current = id[idx]; if (current === 90) { id[idx] = 97; } else if (current === 122) { id[idx] = 65; incrementId(id, idx - 1); } else { id[idx] = current + 1; } } function createPrivateUidGeneratorForClass(classPath) { const currentPrivateId = []; const privateNames = new Set(); types.traverseFast(classPath.node, node => { if (types.isPrivateName(node)) { privateNames.add(node.id.name); } }); return () => { let reifiedId; do { incrementId(currentPrivateId); reifiedId = String.fromCharCode(...currentPrivateId); } while (privateNames.has(reifiedId)); return types.privateName(types.identifier(reifiedId)); }; } function createLazyPrivateUidGeneratorForClass(classPath) { let generator; return () => { if (!generator) { generator = createPrivateUidGeneratorForClass(classPath); } return generator(); }; } function replaceClassWithVar(path, className) { const id = path.node.id; const scope = path.scope; if (path.type === "ClassDeclaration") { const className = id.name; const varId = scope.generateUidIdentifierBasedOnNode(id); const classId = types.identifier(className); scope.rename(className, varId.name); path.get("id").replaceWith(classId); return { id: types.cloneNode(varId), path }; } else { let varId; if (id) { className = id.name; varId = generateLetUidIdentifier(scope.parent, className); scope.rename(className, varId.name); } else { varId = generateLetUidIdentifier(scope.parent, typeof className === "string" ? className : "decorated_class"); } const newClassExpr = types.classExpression(typeof className === "string" ? types.identifier(className) : null, path.node.superClass, path.node.body); const [newPath] = path.replaceWith(types.sequenceExpression([newClassExpr, varId])); return { id: types.cloneNode(varId), path: newPath.get("expressions.0") }; } } function generateClassProperty(key, value, isStatic) { if (key.type === "PrivateName") { return types.classPrivateProperty(key, value, undefined, isStatic); } else { return types.classProperty(key, value, undefined, undefined, isStatic); } } function assignIdForAnonymousClass(path, className) { if (!path.node.id) { path.node.id = typeof className === "string" ? types.identifier(className) : path.scope.generateUidIdentifier("Class"); } } function addProxyAccessorsFor(className, element, getterKey, setterKey, targetKey, isComputed, isStatic) { const thisArg = isStatic ? className : types.thisExpression(); const getterBody = types.blockStatement([types.returnStatement(types.memberExpression(types.cloneNode(thisArg), types.cloneNode(targetKey)))]); const setterBody = types.blockStatement([types.expressionStatement(types.assignmentExpression("=", types.memberExpression(types.cloneNode(thisArg), types.cloneNode(targetKey)), types.identifier("v")))]); let getter, setter; if (getterKey.type === "PrivateName") { getter = types.classPrivateMethod("get", getterKey, [], getterBody, isStatic); setter = types.classPrivateMethod("set", setterKey, [types.identifier("v")], setterBody, isStatic); } else { getter = types.classMethod("get", getterKey, [], getterBody, isComputed, isStatic); setter = types.classMethod("set", setterKey, [types.identifier("v")], setterBody, isComputed, isStatic); } element.insertAfter(setter); element.insertAfter(getter); } function extractProxyAccessorsFor(targetKey) { return [template.expression.ast` o => o.${types.cloneNode(targetKey)} `, template.expression.ast` (o, v) => o.${types.cloneNode(targetKey)} = v `]; } function getComputedKeyLastElement(path) { path = skipTransparentExprWrappers(path); if (path.isSequenceExpression()) { const expressions = path.get("expressions"); return getComputedKeyLastElement(expressions[expressions.length - 1]); } return path; } function getComputedKeyMemoiser(path) { const element = getComputedKeyLastElement(path); if (element.isConstantExpression()) { return types.cloneNode(path.node); } else if (element.isIdentifier() && path.scope.hasUid(element.node.name)) { return types.cloneNode(path.node); } else if (element.isAssignmentExpression() && element.get("left").isIdentifier()) { return types.cloneNode(element.node.left); } else { throw new Error(`Internal Error: the computed key ${path.toString()} has not yet been memoised.`); } } function prependExpressionsToComputedKey(expressions, fieldPath) { const key = fieldPath.get("key"); if (key.isSequenceExpression()) { expressions.push(...key.node.expressions); } else { expressions.push(key.node); } key.replaceWith(maybeSequenceExpression(expressions)); } function appendExpressionsToComputedKey(expressions, fieldPath) { const key = fieldPath.get("key"); const completion = getComputedKeyLastElement(key); if (completion.isConstantExpression()) { prependExpressionsToComputedKey(expressions, fieldPath); } else { const scopeParent = key.scope.parent; const maybeAssignment = memoiseComputedKey(completion.node, scopeParent, scopeParent.generateUid("computedKey")); if (!maybeAssignment) { prependExpressionsToComputedKey(expressions, fieldPath); } else { const expressionSequence = [...expressions, types.cloneNode(maybeAssignment.left)]; const completionParent = completion.parentPath; if (completionParent.isSequenceExpression()) { completionParent.pushContainer("expressions", expressionSequence); } else { completion.replaceWith(maybeSequenceExpression([types.cloneNode(maybeAssignment), ...expressionSequence])); } } } } function prependExpressionsToFieldInitializer(expressions, fieldPath) { const initializer = fieldPath.get("value"); if (initializer.node) { expressions.push(initializer.node); } else if (expressions.length > 0) { expressions[expressions.length - 1] = types.unaryExpression("void", expressions[expressions.length - 1]); } initializer.replaceWith(maybeSequenceExpression(expressions)); } function prependExpressionsToStaticBlock(expressions, blockPath) { blockPath.unshiftContainer("body", types.expressionStatement(maybeSequenceExpression(expressions))); } function prependExpressionsToConstructor(expressions, constructorPath) { constructorPath.node.body.body.unshift(types.expressionStatement(maybeSequenceExpression(expressions))); } function isProtoInitCallExpression(expression, protoInitCall) { return types.isCallExpression(expression) && types.isIdentifier(expression.callee, { name: protoInitCall.name }); } function optimizeSuperCallAndExpressions(expressions, protoInitLocal) { if (protoInitLocal) { if (expressions.length >= 2 && isProtoInitCallExpression(expressions[1], protoInitLocal)) { const mergedSuperCall = types.callExpression(types.cloneNode(protoInitLocal), [expressions[0]]); expressions.splice(0, 2, mergedSuperCall); } if (expressions.length >= 2 && types.isThisExpression(expressions[expressions.length - 1]) && isProtoInitCallExpression(expressions[expressions.length - 2], protoInitLocal)) { expressions.splice(expressions.length - 1, 1); } } return maybeSequenceExpression(expressions); } function insertExpressionsAfterSuperCallAndOptimize(expressions, constructorPath, protoInitLocal) { constructorPath.traverse({ CallExpression: { exit(path) { if (!path.get("callee").isSuper()) return; const newNodes = [path.node, ...expressions.map(expr => types.cloneNode(expr))]; if (path.isCompletionRecord()) { newNodes.push(types.thisExpression()); } path.replaceWith(optimizeSuperCallAndExpressions(newNodes, protoInitLocal)); path.skip(); } }, ClassMethod(path) { if (path.node.kind === "constructor") { path.skip(); } } }); } function createConstructorFromExpressions(expressions, isDerivedClass) { const body = [types.expressionStatement(maybeSequenceExpression(expressions))]; if (isDerivedClass) { body.unshift(types.expressionStatement(types.callExpression(types.super(), [types.spreadElement(types.identifier("args"))]))); } return types.classMethod("constructor", types.identifier("constructor"), isDerivedClass ? [types.restElement(types.identifier("args"))] : [], types.blockStatement(body)); } function createStaticBlockFromExpressions(expressions) { return types.staticBlock([types.expressionStatement(maybeSequenceExpression(expressions))]); } const FIELD = 0; const ACCESSOR = 1; const METHOD = 2; const GETTER = 3; const SETTER = 4; const FIELD_IGNORED = 5; const STATIC = 8; const DECORATORS_HAVE_THIS = 16; function getElementKind({ node }) { switch (node.type) { case "ClassProperty": return node.declare || node.abstract ? FIELD_IGNORED : FIELD; case "ClassPrivateProperty": return FIELD; case "ClassAccessorProperty": return ACCESSOR; case "ClassMethod": case "ClassPrivateMethod": if (node.kind === "get") { return GETTER; } else if (node.kind === "set") { return SETTER; } else { return METHOD; } } } function toSortedDecoratorInfo(info) { return [...info.filter(el => el.isStatic && el.kind >= ACCESSOR && el.kind <= SETTER), ...info.filter(el => !el.isStatic && el.kind >= ACCESSOR && el.kind <= SETTER), ...info.filter(el => el.isStatic && el.kind === FIELD), ...info.filter(el => !el.isStatic && el.kind === FIELD)]; } function generateDecorationList(decorators, decoratorsThis) { const decsCount = decorators.length; const haveOneThis = decoratorsThis.some(Boolean); const decs = []; for (let i = 0; i < decsCount; i++) { if (haveOneThis) { decs.push(decoratorsThis[i] || types.buildUndefinedNode()); } decs.push(decorators[i].expression); } return { haveThis: haveOneThis, decs }; } function generateDecorationExprs(decorationInfo) { return types.arrayExpression(decorationInfo.map(el => { let flag = el.kind; if (el.isStatic) { flag += STATIC; } if (el.decoratorsHaveThis) flag += DECORATORS_HAVE_THIS; return types.arrayExpression([el.decoratorsArray, types.numericLiteral(flag), el.name, ...(el.privateMethods || [])]); })); } function extractElementLocalAssignments(decorationInfo) { const localIds = []; for (const el of decorationInfo) { const { locals } = el; if (Array.isArray(locals)) { localIds.push(...locals); } else if (locals !== undefined) { localIds.push(locals); } } return localIds; } function addCallAccessorsFor(element, key, getId, setId, isStatic) { element.insertAfter(types.classPrivateMethod("get", types.cloneNode(key), [], types.blockStatement([types.returnStatement(types.callExpression(types.cloneNode(getId), isStatic ? [] : [types.thisExpression()]))]), isStatic)); element.insertAfter(types.classPrivateMethod("set", types.cloneNode(key), [types.identifier("v")], types.blockStatement([types.expressionStatement(types.callExpression(types.cloneNode(setId), isStatic ? [types.identifier("v")] : [types.thisExpression(), types.identifier("v")]))]), isStatic)); } function movePrivateAccessor(element, key, methodLocalVar, isStatic) { let params; let block; if (element.node.kind === "set") { params = [types.identifier("v")]; block = [types.expressionStatement(types.callExpression(methodLocalVar, [types.thisExpression(), types.identifier("v")]))]; } else { params = []; block = [types.returnStatement(types.callExpression(methodLocalVar, [types.thisExpression()]))]; } element.replaceWith(types.classPrivateMethod(element.node.kind, types.cloneNode(key), params, types.blockStatement(block), isStatic)); } function isClassDecoratableElementPath(path) { const { type } = path; return type !== "TSDeclareMethod" && type !== "TSIndexSignature" && type !== "StaticBlock"; } function staticBlockToIIFE(block) { return types.callExpression(types.arrowFunctionExpression([], types.blockStatement(block.body)), []); } function staticBlockToFunctionClosure(block) { return types.functionExpression(null, [], types.blockStatement(block.body)); } function fieldInitializerToClosure(value) { return types.functionExpression(null, [], types.blockStatement([types.returnStatement(value)])); } function maybeSequenceExpression(exprs) { if (exprs.length === 0) return types.buildUndefinedNode(); if (exprs.length === 1) return exprs[0]; return types.sequenceExpression(exprs); } function createFunctionExpressionFromPrivateMethod(node) { const { params, body, generator: isGenerator, async: isAsync } = node; return types.functionExpression(undefined, params, body, isGenerator, isAsync); } function createSetFunctionNameCall(state, className) { return types.callExpression(state.addHelper("setFunctionName"), [types.thisExpression(), className]); } function createToPropertyKeyCall(state, propertyKe