@monstermann/babel-plugin-match
Version:
Zero-runtime exhaustive pattern matching.
339 lines (322 loc) • 11.6 kB
JavaScript
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 };