UNPKG

@monstermann/babel-plugin-match

Version:

Zero-runtime exhaustive pattern matching.

339 lines (322 loc) 11.6 kB
import * as t$9 from "@babel/types"; import * as t$8 from "@babel/types"; import * as t$7 from "@babel/types"; import * as t$6 from "@babel/types"; import * as t$5 from "@babel/types"; import * as t$4 from "@babel/types"; import * as t$3 from "@babel/types"; import * as t$2 from "@babel/types"; import * as t$1 from "@babel/types"; import * as t from "@babel/types"; //#region src/buildCase.ts function buildCase(ctx, call, value) { const [pattern, result] = call.node.arguments; ctx.assertExpression(pattern); ctx.assertExpression(result); ctx.addBranch([t$9.binaryExpression("===", value, pattern), result]); } //#endregion //#region src/helpers.ts var AbortError = class extends Error {}; function isAbortError(value) { return value instanceof AbortError; } function* findCallExpressions(path) { let currentPath = path.parentPath; while (currentPath) { if (currentPath.isCallExpression()) yield currentPath; currentPath = currentPath.parentPath; } } function isWellFormedObject(value) { if (!t$8.isObjectExpression(value)) return false; for (const property of value.properties) { if (!t$8.isObjectProperty(property)) return false; if (property.computed) return false; if (!t$8.isExpression(property.key)) return false; if (!t$8.isExpression(property.value)) return false; } return true; } function equals(a, b) { if (a.type === "Identifier" && b.type === "Identifier") return a.name === b.name; if (a.type === "StringLiteral" && b.type === "StringLiteral") return a.value === b.value; if (a.type === "NumericLiteral" && b.type === "NumericLiteral") return a.value === b.value; return false; } function getExpression(lazyExpression) { return typeof lazyExpression === "function" ? lazyExpression() : lazyExpression; } function createThrowExpression(value) { const message = t$8.templateLiteral([t$8.templateElement({ raw: "Pattern matching error: no pattern matches value " }), t$8.templateElement({ raw: "" })], [t$8.callExpression(t$8.memberExpression(t$8.identifier("JSON"), t$8.identifier("stringify")), [value])]); const error = t$8.newExpression(t$8.identifier("Error"), [message]); const throwStatement = t$8.throwStatement(error); return t$8.callExpression(t$8.arrowFunctionExpression([], t$8.blockStatement([throwStatement])), []); } //#endregion //#region src/optimizeCall.ts function optimizeCall(ctx, pattern, value) { if (t$7.isIdentifier(value) && pattern.isIdentifier()) return t$7.callExpression(pattern.node, [value]); if (!pattern.isFunctionExpression() && !pattern.isArrowFunctionExpression()) ctx.abort(); const { body, hasParams, param } = parseFunction(pattern); if (!hasParams && !body) return t$7.identifier("undefined"); if (!hasParams && body?.isExpression()) return body.node; if (!hasParams && body?.isBlockStatement()) return t$7.callExpression(pattern.node, []); if (param && body?.isBlockStatement()) return t$7.callExpression(pattern.node, [value]); if (param && body?.isExpression() && t$7.isIdentifier(value)) return replaceIdentifierReferences(ctx, body, param.node, value); if (param && body?.isExpression() && isWellFormedObject(value)) return replaceObjectReferences(ctx, body, param.node, value); ctx.abort(); } function replaceIdentifierReferences(ctx, body, param, value) { body.traverse({ Identifier(p) { if (p.node.name !== param.name) return; const binding = p.scope.getBinding(p.node.name); if (binding?.identifier !== param) return; ctx.onBeforeBuild(() => p.replaceWith(value)); } }); return () => body.node; } function replaceObjectReferences(ctx, body, param, value) { body.traverse({ Identifier(p) { if (p.node.name !== param.name) return; const binding = p.scope.getBinding(p.node.name); if (binding?.identifier !== param) return; if (!t$7.isMemberExpression(p.parent)) ctx.abort(); const member = p.parent.property; if (!t$7.isExpression(member)) ctx.abort(); const property = value.properties.find((property$1) => equals(member, property$1.key)); if (!property) ctx.abort(); ctx.onBeforeBuild(() => p.parentPath.replaceWith(property.value)); } }); return () => body.node; } function parseFunction(pattern) { return { body: parseBody(pattern), hasParams: pattern.node.params.length > 0, param: parseParam(pattern) }; } function parseParam(pattern) { if (pattern.node.params.length !== 1) return; const param = pattern.get("params.0"); if (Array.isArray(param)) return; return param.isIdentifier() ? param : void 0; } function parseBody(pattern) { const body = pattern.get("body"); if (Array.isArray(body)) return; if (body.isExpression()) return body; if (body.isBlockStatement() && body.get("body.0").isReturnStatement() && body.get("body.0.argument").isExpression()) return body.get("body.0.argument"); if (body.isBlockStatement()) return body; return void 0; } //#endregion //#region src/buildCond.ts function buildCond(ctx, call, value) { const [pattern, result] = call.node.arguments; ctx.assertExpression(pattern); ctx.assertExpression(result); ctx.addBranch([optimizeCall(ctx, call.get("arguments.0"), value), result]); } //#endregion //#region src/buildOnCase.ts function buildOnCase(ctx, call, value) { const [pattern, result] = call.node.arguments; ctx.assertExpression(pattern); ctx.assertExpression(result); ctx.addBranch([t$6.binaryExpression("===", value, pattern), optimizeCall(ctx, call.get("arguments.1"), value)]); } //#endregion //#region src/buildOnCond.ts function buildOnCond(ctx, call, value) { const [pattern, result] = call.node.arguments; if (!t$5.isExpression(pattern)) ctx.abort(); if (!t$5.isExpression(result)) ctx.abort(); ctx.addBranch([optimizeCall(ctx, call.get("arguments.0"), value), optimizeCall(ctx, call.get("arguments.1"), value)]); } //#endregion //#region src/optimizeShape.ts function optimizeShape(ctx, pattern, value) { if (isWellFormedObject(value) && isWellFormedObject(pattern)) { let result; for (const propertyRight of pattern.properties) { const propertyLeft = value.properties.find((propertyLeft$1) => equals(propertyLeft$1.key, propertyRight.key)); if (!propertyLeft) ctx.abort(); const test = t$4.binaryExpression("===", propertyLeft.value, propertyRight.value); result = result ? t$4.logicalExpression("&&", result, test) : test; } if (!result) ctx.abort(); return result; } if (t$4.isIdentifier(value) && isWellFormedObject(pattern)) { let result = t$4.logicalExpression("&&", value, t$4.binaryExpression("===", t$4.unaryExpression("typeof", value), t$4.stringLiteral("object"))); for (const property of pattern.properties) { const isComputed = property.computed || t$4.isStringLiteral(property.key) || t$4.isNumericLiteral(property.key); const test = t$4.binaryExpression("===", t$4.memberExpression(value, property.key, isComputed), property.value); result = t$4.logicalExpression("&&", result, test); } return result; } ctx.abort(); } //#endregion //#region src/buildOnShape.ts function buildOnShape(ctx, call, value) { const [pattern, result] = call.node.arguments; ctx.assertObjectExpression(pattern); ctx.assertExpression(result); ctx.addBranch([optimizeShape(ctx, pattern, value), optimizeCall(ctx, call.get("arguments.1"), value)]); } //#endregion //#region src/buildOr.ts function buildOr(ctx, call, _value) { const [fallback] = call.node.arguments; ctx.assertExpression(fallback); ctx.build(call, fallback); } //#endregion //#region src/buildOrElse.ts function buildOrElse(ctx, call, value) { const [fallback] = call.node.arguments; ctx.assertExpression(fallback); ctx.build(call, optimizeCall(ctx, call.get("arguments.0"), value)); } //#endregion //#region src/buildOrThrow.ts function buildOrThrow(ctx, call, value) { ctx.build(call, createThrowExpression(value)); } //#endregion //#region src/buildShape.ts function buildShape(ctx, call, value) { const [pattern, result] = call.node.arguments; ctx.assertObjectExpression(pattern); ctx.assertExpression(result); ctx.addBranch([optimizeShape(ctx, pattern, value), result]); } //#endregion //#region src/Context.ts var Context = class { after = new Set(); before = new Set(); branches = []; path; constructor(path) { this.path = path; } abort() { throw new AbortError(); } addBranch(branch) { this.branches.push(branch); } assertExpression(value) { if (!t$3.isExpression(value)) this.abort(); } assertObjectExpression(value) { if (!t$3.isObjectExpression(value)) this.abort(); } build(path, fallback) { for (const cb of this.before) cb(); let expression = getExpression(fallback); let i = this.branches.length; while (i--) { const [test, result] = this.branches[i]; expression = t$3.conditionalExpression(getExpression(test), getExpression(result), expression); } for (const cb of this.after) expression = cb(expression); path.replaceWith(expression); } onAfterBuild(cb) { this.after.add(cb); } onBeforeBuild(cb) { this.before.add(cb); } }; //#endregion //#region src/build.ts function build(path, _value, hoist = false) { let value = _value; const ctx = new Context(path); if (hoist) { const id = path.scope.generateUidIdentifier("value"); value = id; ctx.onAfterBuild((expr) => { return t$2.callExpression(t$2.arrowFunctionExpression([id], expr), [_value]); }); } try { for (const call of findCallExpressions(path)) { const node = call.node; if (!t$2.isMemberExpression(node.callee)) continue; if (!t$2.isIdentifier(node.callee.property)) return ctx.abort(); const name = node.callee.property.name; if (name === "returnType") continue; if (name === "case") buildCase(ctx, call, value); else if (name === "onCase") buildOnCase(ctx, call, value); else if (name === "shape") buildShape(ctx, call, value); else if (name === "onShape") buildOnShape(ctx, call, value); else if (name === "cond") buildCond(ctx, call, value); else if (name === "onCond") buildOnCond(ctx, call, value); else if (name === "or") buildOr(ctx, call, value); else if (name === "orElse") buildOrElse(ctx, call, value); else if (name === "orThrow") buildOrThrow(ctx, call, value); else ctx.abort(); } } catch (error) { if (!isAbortError(error)) throw error; if (hoist) return; if (t$2.isIdentifier(value)) return; build(path, value, true); } } //#endregion //#region src/imports.ts const imports = new Map(); function resetImports() { imports.clear(); } function collectImports(path) { const node = path.node; if (node.source.value !== "@monstermann/match") return; for (const specifier of node.specifiers) { if (!t$1.isImportSpecifier(specifier)) continue; if (t$1.isIdentifier(specifier.imported)) imports.set(specifier.imported.name, specifier.local); else if (t$1.isStringLiteral(specifier.imported)) imports.set(specifier.imported.value, specifier.local); } } function hasMatchImport() { return imports.has("match"); } function isMatchImport(path) { const matchImport = imports.get("match"); if (!matchImport) return false; if (!t$1.isIdentifier(path.node.callee)) return false; return path.node.callee.name === matchImport.name; } //#endregion //#region src/index.ts function src_default() { return { name: "@monstermann/unplugin-match", visitor: { Program: { enter() { resetImports(); } }, CallExpression(path) { if (!hasMatchImport()) return; if (!isMatchImport(path)) return; const value = path.node.arguments[0]; if (!t.isExpression(value)) return; build(path, value); }, ImportDeclaration(path) { collectImports(path); } } }; } //#endregion export { src_default as default };