mirror of
https://github.com/duhanbalci/dexpr.git
synced 2026-07-02 00:29:15 +00:00
185 lines
6.5 KiB
JavaScript
185 lines
6.5 KiB
JavaScript
import { NodeProp } from '@lezer/common';
|
|
|
|
const none = [];
|
|
class TestSpec {
|
|
constructor(name, props, children = none, wildcard = false) {
|
|
this.name = name;
|
|
this.props = props;
|
|
this.children = children;
|
|
this.wildcard = wildcard;
|
|
}
|
|
static parse(spec) {
|
|
let pos = 0, tok = "sof", value = "";
|
|
function err() {
|
|
throw new SyntaxError("Invalid test spec: " + spec);
|
|
}
|
|
function next() {
|
|
while (pos < spec.length && /\s/.test(spec.charAt(pos)))
|
|
pos++;
|
|
if (pos == spec.length)
|
|
return tok = "eof";
|
|
let next = spec.charAt(pos++);
|
|
if (next == "(" && spec.slice(pos, pos + 4) == "...)") {
|
|
pos += 4;
|
|
return tok = "...";
|
|
}
|
|
if (/[\[\](),=]/.test(next))
|
|
return tok = next;
|
|
if (/[^()\[\],="\s]/.test(next)) {
|
|
let name = /[^()\[\],="\s]*/.exec(spec.slice(pos - 1));
|
|
value = name[0];
|
|
pos += name[0].length - 1;
|
|
return tok = "name";
|
|
}
|
|
if (next == '"') {
|
|
let content = /^"((?:[^\\"]|\\.)*)"/.exec(spec.slice(pos - 1)) || err();
|
|
value = JSON.parse(content[0]);
|
|
pos += content[0].length - 1;
|
|
return tok = "name";
|
|
}
|
|
return err();
|
|
}
|
|
next();
|
|
function parseSeq() {
|
|
let seq = [];
|
|
while (tok != "eof" && tok != ")") {
|
|
seq.push(parse());
|
|
if (tok == ",")
|
|
next();
|
|
}
|
|
return seq;
|
|
}
|
|
function parse() {
|
|
let name = value, children = none, props = [], wildcard = false;
|
|
if (tok != "name")
|
|
err();
|
|
next();
|
|
if (tok == "[") {
|
|
next();
|
|
while (tok != "]") {
|
|
if (tok != "name")
|
|
err();
|
|
let prop = NodeProp[value], val = "";
|
|
if (!(prop instanceof NodeProp))
|
|
err();
|
|
next();
|
|
if (tok == "=") {
|
|
next();
|
|
if (tok != "name")
|
|
err();
|
|
val = value;
|
|
next();
|
|
}
|
|
props.push({ prop, value: prop.deserialize(val) });
|
|
}
|
|
next();
|
|
}
|
|
if (tok == "(") {
|
|
next();
|
|
children = parseSeq();
|
|
// @ts-ignore TypeScript doesn't understand that `next` may have mutated `tok` (#9998)
|
|
if (tok != ")")
|
|
err();
|
|
next();
|
|
}
|
|
else if (tok == "...") {
|
|
wildcard = true;
|
|
next();
|
|
}
|
|
return new TestSpec(name, props, children, wildcard);
|
|
}
|
|
let result = parseSeq();
|
|
if (tok != "eof")
|
|
err();
|
|
return result;
|
|
}
|
|
matches(type) {
|
|
if (type.name != this.name)
|
|
return false;
|
|
for (let { prop, value } of this.props)
|
|
if ((value || type.prop(prop)) && JSON.stringify(type.prop(prop)) != JSON.stringify(value))
|
|
return false;
|
|
return true;
|
|
}
|
|
}
|
|
function defaultIgnore(type) { return /\W/.test(type.name); }
|
|
function testTree(tree, expect, mayIgnore = defaultIgnore) {
|
|
let specs = TestSpec.parse(expect);
|
|
let stack = [specs], pos = [0];
|
|
tree.iterate({
|
|
enter(n) {
|
|
if (!n.name)
|
|
return;
|
|
let last = stack.length - 1, index = pos[last], seq = stack[last];
|
|
let next = index < seq.length ? seq[index] : null;
|
|
if (next && next.matches(n.type)) {
|
|
if (next.wildcard) {
|
|
pos[last]++;
|
|
return false;
|
|
}
|
|
pos.push(0);
|
|
stack.push(next.children);
|
|
return undefined;
|
|
}
|
|
else if (mayIgnore(n.type)) {
|
|
return false;
|
|
}
|
|
else {
|
|
let parent = last > 0 ? stack[last - 1][pos[last - 1]].name : "tree";
|
|
let after = next ? next.name + (parent == "tree" ? "" : " in " + parent) : `end of ${parent}`;
|
|
throw new Error(`Expected ${after}, got ${n.name} at ${n.to} \n${tree}`);
|
|
}
|
|
},
|
|
leave(n) {
|
|
if (!n.name)
|
|
return;
|
|
let last = stack.length - 1, index = pos[last], seq = stack[last];
|
|
if (index < seq.length)
|
|
throw new Error(`Unexpected end of ${n.name}. Expected ${seq.slice(index).map(s => s.name).join(", ")} at ${n.from}\n${tree}`);
|
|
pos.pop();
|
|
stack.pop();
|
|
pos[last - 1]++;
|
|
}
|
|
});
|
|
if (pos[0] != specs.length)
|
|
throw new Error(`Unexpected end of tree. Expected ${stack[0].slice(pos[0]).map(s => s.name).join(", ")} at ${tree.length}\n${tree}`);
|
|
}
|
|
function toLineContext(file, index) {
|
|
const endEol = file.indexOf('\n', index + 80);
|
|
const endIndex = endEol === -1 ? file.length : endEol;
|
|
return file.substring(index, endIndex).split(/\n/).map(str => ' | ' + str).join('\n');
|
|
}
|
|
function fileTests(file, fileName, mayIgnore = defaultIgnore) {
|
|
let caseExpr = /\s*#[ \t]*(.*)(?:\r\n|\r|\n)([^]*?)==+>([^]*?)(?:$|(?:\r\n|\r|\n)+(?=#))/gy;
|
|
let tests = [];
|
|
let lastIndex = 0;
|
|
for (;;) {
|
|
let m = caseExpr.exec(file);
|
|
if (!m)
|
|
throw new Error(`Unexpected file format in ${fileName} around\n\n${toLineContext(file, lastIndex)}`);
|
|
let text = m[2].trim(), expected = m[3].trim();
|
|
let [, name, configStr] = /(.*?)(\{.*?\})?$/.exec(m[1]);
|
|
let config = configStr ? JSON.parse(configStr) : null;
|
|
let strict = !/⚠|\.\.\./.test(expected);
|
|
tests.push({
|
|
name,
|
|
text,
|
|
expected,
|
|
configStr,
|
|
config,
|
|
strict,
|
|
run(parser) {
|
|
if (parser.configure && (strict || config))
|
|
parser = parser.configure(Object.assign({ strict }, config));
|
|
testTree(parser.parse(text), expected, mayIgnore);
|
|
}
|
|
});
|
|
lastIndex = m.index + m[0].length;
|
|
if (lastIndex == file.length)
|
|
break;
|
|
}
|
|
return tests;
|
|
}
|
|
|
|
export { fileTests, testTree };
|