Skip to content

Commit 64bd12b

Browse files
committed
Pattern Matching WIP
1 parent eb650ac commit 64bd12b

File tree

15 files changed

+291
-4
lines changed

15 files changed

+291
-4
lines changed

src/index.js

Lines changed: 193 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,9 @@ export default function (babel) {
255255
return body;
256256
}
257257

258-
function ensureBlockBody(path) {
259-
if (!t.isBlockStatement(path.node.body)) {
260-
path.get("body").replaceWith(t.blockStatement([path.node.body]));
258+
function ensureBlockBody(path, bodyKey = "body") {
259+
if (!path.get(bodyKey).isBlockStatement()) {
260+
path.get(bodyKey).replaceWith(t.blockStatement([path.node[bodyKey]]));
261261
}
262262
}
263263

@@ -663,7 +663,7 @@ export default function (babel) {
663663
}
664664
}
665665

666-
function generateForInIterator (path, type: "array" | "object") {
666+
function generateForInIterator(path, type: "array" | "object") {
667667
const idx = path.node.idx || path.scope.generateUidIdentifier("i");
668668
const len = path.scope.generateUidIdentifier("len");
669669

@@ -758,6 +758,117 @@ export default function (babel) {
758758
return t.forStatement(init, test, update, path.node.body);
759759
}
760760

761+
function addAlternateToIfChain(rootIf, alternate) {
762+
let tail = rootIf
763+
while (tail.alternate) tail = tail.alternate;
764+
tail.alternate = alternate;
765+
return rootIf;
766+
}
767+
768+
function isUndefined(path) {
769+
return path.isIdentifier() && path.node.name === "undefined";
770+
}
771+
772+
function isSignedNumber(path) {
773+
return path.isUnaryExpression() &&
774+
path.get("argument").isNumericLiteral() &&
775+
(path.node.operator === "+" || path.node.operator === "-");
776+
}
777+
778+
function looksLikeClassName(path) {
779+
// disallow Foo.Bar for now.
780+
if (!path.isIdentifier()) return false;
781+
const { name } = path.node;
782+
if (name[0].toUpperCase() === name[0]) {
783+
// A -> true
784+
if (name.length === 1) return true;
785+
// ABC -> false, it's a constant
786+
if (name.toUpperCase() === name) return false;
787+
// Abc -> true
788+
return true;
789+
}
790+
return false;
791+
}
792+
793+
function isPrimitiveClass({ node: { name }}) {
794+
switch (name) {
795+
case "Number":
796+
case "String":
797+
case "Boolean":
798+
return true;
799+
default:
800+
return false;
801+
}
802+
}
803+
804+
function transformMatchCaseTest(path, argRef) {
805+
if (path.isLogicalExpression()) {
806+
transformMatchCaseTest(path.get("left"), argRef);
807+
transformMatchCaseTest(path.get("right"), argRef);
808+
} else if (path.isUnaryExpression() && path.node.operator === "!") {
809+
transformMatchCaseTest(path.get("argument"), argRef);
810+
} else if (path.isRegExpLiteral()) {
811+
const testCall = t.callExpression(
812+
t.memberExpression(path.node, t.identifier("test")),
813+
[argRef]
814+
);
815+
path.replaceWith(testCall);
816+
} else if (looksLikeClassName(path)) {
817+
const classInstanceCheck = isPrimitiveClass(path)
818+
? t.binaryExpression("===",
819+
t.unaryExpression("typeof", argRef),
820+
t.stringLiteral(path.node.name.toLowerCase())
821+
)
822+
: t.binaryExpression("instanceof", argRef, path.node)
823+
path.replaceWith(classInstanceCheck);
824+
} else if (path.isLiteral() || isUndefined(path) || isSignedNumber(path)) {
825+
const isEq = t.binaryExpression("===", argRef, path.node);
826+
path.replaceWith(isEq);
827+
}
828+
}
829+
830+
function transformMatchCases(argRef, cases) {
831+
return cases.reduce((rootIf, path) => {
832+
833+
// fill in placeholders
834+
path.get("test").traverse({
835+
PlaceholderExpression(placeholderPath) {
836+
placeholderPath.replaceWith(argRef);
837+
}
838+
});
839+
840+
// add in ===, etc
841+
transformMatchCaseTest(path.get("test"), argRef);
842+
843+
// add binding (and always use block bodies)
844+
ensureBlockBody(path, "consequent");
845+
if (path.node.binding) {
846+
const bindingDecl = t.variableDeclaration("const", [
847+
t.variableDeclarator(path.node.binding, argRef)
848+
]);
849+
path.get("consequent").unshiftContainer("body", bindingDecl);
850+
}
851+
852+
// handle `else`
853+
if (path.node.test.type === "MatchElse") {
854+
if (rootIf) {
855+
return addAlternateToIfChain(rootIf, path.node.consequent);
856+
} else {
857+
// single match case with "else", weird. Just generate an anonymous block for now.
858+
return path.node.consequent;
859+
}
860+
}
861+
862+
// generate `if` and append to if-else chain
863+
const ifStmt = t.ifStatement(path.node.test, path.node.consequent);
864+
if (!rootIf) {
865+
return ifStmt;
866+
} else {
867+
return addAlternateToIfChain(rootIf, ifStmt);
868+
}
869+
}, null);
870+
}
871+
761872
// TYPE DEFINITIONS
762873
definePluginType("ForInArrayStatement", {
763874
visitor: ["idx", "elem", "array", "body"],
@@ -960,6 +1071,55 @@ export default function (babel) {
9601071
aliases: ["MemberExpression", "Expression", "LVal"],
9611072
});
9621073

1074+
definePluginType("MatchExpression", {
1075+
builder: ["discriminant", "cases"],
1076+
visitor: ["discriminant", "cases"],
1077+
aliases: ["Expression", "Conditional"],
1078+
fields: {
1079+
discriminant: {
1080+
validate: assertNodeType("Expression")
1081+
},
1082+
cases: {
1083+
validate: chain(assertValueType("array"), assertEach(assertNodeType("MatchCase")))
1084+
}
1085+
}
1086+
});
1087+
1088+
definePluginType("MatchStatement", {
1089+
builder: ["discriminant", "cases"],
1090+
visitor: ["discriminant", "cases"],
1091+
aliases: ["Statement", "Conditional"],
1092+
fields: {
1093+
discriminant: {
1094+
validate: assertNodeType("Expression")
1095+
},
1096+
cases: {
1097+
validate: chain(assertValueType("array"), assertEach(assertNodeType("MatchCase")))
1098+
}
1099+
}
1100+
});
1101+
1102+
definePluginType("MatchCase", {
1103+
builder: ["test", "consequent", "functional"],
1104+
visitor: ["test", "consequent"],
1105+
fields: {
1106+
test: {
1107+
validate: assertNodeType("Expression", "MatchElse")
1108+
},
1109+
consequent: {
1110+
validate: assertNodeType("BlockStatement", "ExpressionStatement")
1111+
}
1112+
}
1113+
});
1114+
1115+
definePluginType("MatchElse", {
1116+
// only allowed in MatchCase, so don't alias to Expression
1117+
});
1118+
1119+
definePluginType("PlaceholderExpression", {
1120+
aliases: ["Expression"]
1121+
});
1122+
9631123
// traverse as top-level item so as to run before other babel plugins
9641124
// (and avoid traversing any of their output)
9651125
function Program(path, state) {
@@ -1258,6 +1418,35 @@ export default function (babel) {
12581418
}
12591419
},
12601420

1421+
MatchExpression(path) {
1422+
const { discriminant } = path.node;
1423+
1424+
const argRef = path.scope.generateUidIdentifier("it");
1425+
const matchBody = transformMatchCases(argRef, path.get("cases"));
1426+
1427+
const iife = t.callExpression(
1428+
t.arrowFunctionExpression([argRef], t.blockStatement([matchBody])),
1429+
[discriminant]
1430+
);
1431+
path.replaceWith(iife);
1432+
},
1433+
1434+
MatchStatement(path) {
1435+
const { discriminant } = path.node;
1436+
1437+
let argRef;
1438+
if (t.isIdentifier(discriminant)) {
1439+
argRef = discriminant;
1440+
} else {
1441+
argRef = path.scope.generateUidIdentifier("it");
1442+
path.insertBefore(t.variableDeclaration("const", [
1443+
t.variableDeclarator(argRef, discriminant)
1444+
]));
1445+
}
1446+
1447+
const matchBody = transformMatchCases(argRef, path.get("cases"));
1448+
path.replaceWith(matchBody);
1449+
},
12611450

12621451
});
12631452

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
match x:
2+
| < 1: "lt one"
3+
| + 1 == 1: "eq zero"
4+
| == 2: "eq two"
5+
| ~f(): "f(x) truthy"
6+
| .prop: "has prop"
7+
| .0: "has first child"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
if (x < 1) {
2+
"lt one";
3+
} else if (x + 1 === 1) {
4+
"eq zero";
5+
} else if (x === 2) {
6+
"eq two";
7+
} else if (f(x)) {
8+
"f(x) truthy";
9+
} else if (x.prop) {
10+
"has prop";
11+
} else if (x[0]) {
12+
"has first child";
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
match x:
2+
| 1:
3+
"one"
4+
| 2:
5+
"two"
6+
| else:
7+
"idk"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
if (x === 1) {
2+
"one";
3+
} else if (x === 2) {
4+
"two";
5+
} else {
6+
"idk";
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
match x:
2+
| 1: "lt one"
3+
| "hi": "eq zero"
4+
| `there ${1 + 1}`: "eq two"
5+
| ~f(): "f(x) truthy"
6+
| .prop: "has prop"
7+
| .0: "has first child"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
if (x === 1) {
2+
"lt one";
3+
} else if (x === "hi") {
4+
"eq zero";
5+
} else if (x === `there ${ 1 + 1 }`) {
6+
"eq two";
7+
} else if (f(x)) {
8+
"f(x) truthy";
9+
} else if (x.prop) {
10+
"has prop";
11+
} else if (x[0]) {
12+
"has first child";
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
z = match x:
2+
| 1:
3+
"one"
4+
| 2:
5+
"two"
6+
| else:
7+
"idk"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const z = (_it => {
2+
if (_it === 1) {
3+
return "one";
4+
} else if (_it === 2) {
5+
return "two";
6+
} else {
7+
return "idk";
8+
}
9+
})(x);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
match x:
2+
| 1:
3+
"one"
4+
| 2:
5+
"two"

0 commit comments

Comments
 (0)