forked from github/quartz
feat(bases): migrate from vault to upstream
Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
This commit is contained in:
261
quartz/util/base/compiler/parser.test.ts
Normal file
261
quartz/util/base/compiler/parser.test.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import assert from "node:assert"
|
||||
import test from "node:test"
|
||||
import { parseExpressionSource } from "./parser"
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null
|
||||
|
||||
const strip = (node: unknown): unknown => {
|
||||
if (!isRecord(node)) return node
|
||||
const type = node.type
|
||||
if (type === "Identifier") {
|
||||
return { type, name: node.name }
|
||||
}
|
||||
if (type === "Literal") {
|
||||
const kind = node.kind
|
||||
const value = node.value
|
||||
const flags = node.flags
|
||||
return flags !== undefined ? { type, kind, value, flags } : { type, kind, value }
|
||||
}
|
||||
if (type === "UnaryExpr") {
|
||||
return { type, operator: node.operator, argument: strip(node.argument) }
|
||||
}
|
||||
if (type === "BinaryExpr" || type === "LogicalExpr") {
|
||||
return { type, operator: node.operator, left: strip(node.left), right: strip(node.right) }
|
||||
}
|
||||
if (type === "CallExpr") {
|
||||
const args = Array.isArray(node.args) ? node.args.map(strip) : []
|
||||
return { type, callee: strip(node.callee), args }
|
||||
}
|
||||
if (type === "MemberExpr") {
|
||||
return { type, object: strip(node.object), property: node.property }
|
||||
}
|
||||
if (type === "IndexExpr") {
|
||||
return { type, object: strip(node.object), index: strip(node.index) }
|
||||
}
|
||||
if (type === "ListExpr") {
|
||||
const elements = Array.isArray(node.elements) ? node.elements.map(strip) : []
|
||||
return { type, elements }
|
||||
}
|
||||
if (type === "ErrorExpr") {
|
||||
return { type, message: node.message }
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
test("ebnf to ast mapping snapshots", () => {
|
||||
const cases: Array<{ source: string; expected: unknown }> = [
|
||||
{
|
||||
source: 'status == "done"',
|
||||
expected: {
|
||||
type: "BinaryExpr",
|
||||
operator: "==",
|
||||
left: { type: "Identifier", name: "status" },
|
||||
right: { type: "Literal", kind: "string", value: "done" },
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "!done",
|
||||
expected: {
|
||||
type: "UnaryExpr",
|
||||
operator: "!",
|
||||
argument: { type: "Identifier", name: "done" },
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "file.ctime",
|
||||
expected: {
|
||||
type: "MemberExpr",
|
||||
object: { type: "Identifier", name: "file" },
|
||||
property: "ctime",
|
||||
},
|
||||
},
|
||||
{
|
||||
source: 'note["my-field"]',
|
||||
expected: {
|
||||
type: "IndexExpr",
|
||||
object: { type: "Identifier", name: "note" },
|
||||
index: { type: "Literal", kind: "string", value: "my-field" },
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "date(due) < today()",
|
||||
expected: {
|
||||
type: "BinaryExpr",
|
||||
operator: "<",
|
||||
left: {
|
||||
type: "CallExpr",
|
||||
callee: { type: "Identifier", name: "date" },
|
||||
args: [{ type: "Identifier", name: "due" }],
|
||||
},
|
||||
right: { type: "CallExpr", callee: { type: "Identifier", name: "today" }, args: [] },
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "now() - file.ctime",
|
||||
expected: {
|
||||
type: "BinaryExpr",
|
||||
operator: "-",
|
||||
left: { type: "CallExpr", callee: { type: "Identifier", name: "now" }, args: [] },
|
||||
right: {
|
||||
type: "MemberExpr",
|
||||
object: { type: "Identifier", name: "file" },
|
||||
property: "ctime",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "(pages * 2).round(0)",
|
||||
expected: {
|
||||
type: "CallExpr",
|
||||
callee: {
|
||||
type: "MemberExpr",
|
||||
object: {
|
||||
type: "BinaryExpr",
|
||||
operator: "*",
|
||||
left: { type: "Identifier", name: "pages" },
|
||||
right: { type: "Literal", kind: "number", value: 2 },
|
||||
},
|
||||
property: "round",
|
||||
},
|
||||
args: [{ type: "Literal", kind: "number", value: 0 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
source: 'tags.containsAny("a","b")',
|
||||
expected: {
|
||||
type: "CallExpr",
|
||||
callee: {
|
||||
type: "MemberExpr",
|
||||
object: { type: "Identifier", name: "tags" },
|
||||
property: "containsAny",
|
||||
},
|
||||
args: [
|
||||
{ type: "Literal", kind: "string", value: "a" },
|
||||
{ type: "Literal", kind: "string", value: "b" },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "list(links).filter(value.isTruthy())",
|
||||
expected: {
|
||||
type: "CallExpr",
|
||||
callee: {
|
||||
type: "MemberExpr",
|
||||
object: {
|
||||
type: "CallExpr",
|
||||
callee: { type: "Identifier", name: "list" },
|
||||
args: [{ type: "Identifier", name: "links" }],
|
||||
},
|
||||
property: "filter",
|
||||
},
|
||||
args: [
|
||||
{
|
||||
type: "CallExpr",
|
||||
callee: {
|
||||
type: "MemberExpr",
|
||||
object: { type: "Identifier", name: "value" },
|
||||
property: "isTruthy",
|
||||
},
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
source: '["a", "b", "c"].length',
|
||||
expected: {
|
||||
type: "MemberExpr",
|
||||
object: {
|
||||
type: "ListExpr",
|
||||
elements: [
|
||||
{ type: "Literal", kind: "string", value: "a" },
|
||||
{ type: "Literal", kind: "string", value: "b" },
|
||||
{ type: "Literal", kind: "string", value: "c" },
|
||||
],
|
||||
},
|
||||
property: "length",
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "this.file.name",
|
||||
expected: {
|
||||
type: "MemberExpr",
|
||||
object: {
|
||||
type: "MemberExpr",
|
||||
object: { type: "Identifier", name: "this" },
|
||||
property: "file",
|
||||
},
|
||||
property: "name",
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "a || b && c",
|
||||
expected: {
|
||||
type: "LogicalExpr",
|
||||
operator: "||",
|
||||
left: { type: "Identifier", name: "a" },
|
||||
right: {
|
||||
type: "LogicalExpr",
|
||||
operator: "&&",
|
||||
left: { type: "Identifier", name: "b" },
|
||||
right: { type: "Identifier", name: "c" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "values[0]",
|
||||
expected: {
|
||||
type: "IndexExpr",
|
||||
object: { type: "Identifier", name: "values" },
|
||||
index: { type: "Literal", kind: "number", value: 0 },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
for (const entry of cases) {
|
||||
const result = parseExpressionSource(entry.source)
|
||||
assert.strictEqual(result.diagnostics.length, 0)
|
||||
assert.deepStrictEqual(strip(result.program.body), entry.expected)
|
||||
}
|
||||
})
|
||||
|
||||
test("syntax doc samples parse", () => {
|
||||
const samples = [
|
||||
'note["price"]',
|
||||
"file.size > 10",
|
||||
"file.hasLink(this.file)",
|
||||
'date("2024-12-01") + "1M" + "4h" + "3m"',
|
||||
"now() - file.ctime",
|
||||
"property[0]",
|
||||
'link("filename", icon("plus"))',
|
||||
'file.mtime > now() - "1 week"',
|
||||
'/abc/.matches("abcde")',
|
||||
'name.replace(/:/g, "-")',
|
||||
'values.filter(value.isType("number")).reduce(if(acc == null || value > acc, value, acc), null)',
|
||||
]
|
||||
|
||||
for (const source of samples) {
|
||||
const result = parseExpressionSource(source)
|
||||
assert.strictEqual(result.diagnostics.length, 0)
|
||||
assert.ok(result.program.body)
|
||||
}
|
||||
})
|
||||
|
||||
test("string escapes are decoded", () => {
|
||||
const result = parseExpressionSource('"a\\n\\"b"')
|
||||
assert.strictEqual(result.diagnostics.length, 0)
|
||||
const literal = strip(result.program.body)
|
||||
if (!isRecord(literal)) {
|
||||
throw new Error("expected literal record")
|
||||
}
|
||||
assert.strictEqual(literal.type, "Literal")
|
||||
assert.strictEqual(literal.kind, "string")
|
||||
assert.strictEqual(literal.value, 'a\n"b')
|
||||
})
|
||||
|
||||
test("parser reports errors and recovers", () => {
|
||||
const result = parseExpressionSource("status ==")
|
||||
assert.ok(result.diagnostics.length > 0)
|
||||
assert.ok(result.program.body)
|
||||
})
|
||||
Reference in New Issue
Block a user