This lets wasm clients check whether a query exceeded its maximum number of in-progress matches.
325 lines
9.8 KiB
JavaScript
325 lines
9.8 KiB
JavaScript
const { assert } = require("chai");
|
|
let Parser, JavaScript;
|
|
|
|
describe("Query", () => {
|
|
let parser, tree, query;
|
|
|
|
before(async () => ({ Parser, JavaScript } = await require("./helper")));
|
|
|
|
beforeEach(() => {
|
|
parser = new Parser().setLanguage(JavaScript);
|
|
});
|
|
|
|
afterEach(() => {
|
|
parser.delete();
|
|
if (tree) tree.delete();
|
|
if (query) query.delete();
|
|
});
|
|
|
|
describe("construction", () => {
|
|
it("throws an error on invalid patterns", () => {
|
|
assert.throws(() => {
|
|
JavaScript.query("(function_declaration wat)");
|
|
}, "Bad syntax at offset 22: 'wat)'...");
|
|
assert.throws(() => {
|
|
JavaScript.query("(non_existent)");
|
|
}, "Bad node name 'non_existent'");
|
|
assert.throws(() => {
|
|
JavaScript.query("(a)");
|
|
}, "Bad node name 'a'");
|
|
assert.throws(() => {
|
|
JavaScript.query("(function_declaration non_existent:(identifier))");
|
|
}, "Bad field name 'non_existent'");
|
|
assert.throws(() => {
|
|
JavaScript.query("(function_declaration name:(statement_block))");
|
|
}, "Bad pattern structure at offset 22: 'name:(statement_block))'");
|
|
});
|
|
|
|
it("throws an error on invalid predicates", () => {
|
|
assert.throws(() => {
|
|
JavaScript.query("((identifier) @abc (#eq? @ab hi))");
|
|
}, "Bad capture name @ab");
|
|
assert.throws(() => {
|
|
JavaScript.query("((identifier) @abc (#eq? @ab hi))");
|
|
}, "Bad capture name @ab");
|
|
assert.throws(() => {
|
|
JavaScript.query("((identifier) @abc (#eq?))");
|
|
}, "Wrong number of arguments to `#eq?` predicate. Expected 2, got 0");
|
|
assert.throws(() => {
|
|
JavaScript.query("((identifier) @a (eq? @a @a @a))");
|
|
}, "Wrong number of arguments to `#eq?` predicate. Expected 2, got 3");
|
|
});
|
|
});
|
|
|
|
describe(".matches", () => {
|
|
it("returns all of the matches for the given query", () => {
|
|
tree = parser.parse("function one() { two(); function three() {} }");
|
|
query = JavaScript.query(`
|
|
(function_declaration name: (identifier) @fn-def)
|
|
(call_expression function: (identifier) @fn-ref)
|
|
`);
|
|
const matches = query.matches(tree.rootNode);
|
|
assert.deepEqual(formatMatches(matches), [
|
|
{ pattern: 0, captures: [{ name: "fn-def", text: "one" }] },
|
|
{ pattern: 1, captures: [{ name: "fn-ref", text: "two" }] },
|
|
{ pattern: 0, captures: [{ name: "fn-def", text: "three" }] },
|
|
]);
|
|
});
|
|
|
|
it("can search in a specified ranges", () => {
|
|
tree = parser.parse("[a, b,\nc, d,\ne, f,\ng, h]");
|
|
query = JavaScript.query("(identifier) @element");
|
|
const matches = query.matches(
|
|
tree.rootNode,
|
|
{ row: 1, column: 1 },
|
|
{ row: 3, column: 1 }
|
|
);
|
|
assert.deepEqual(formatMatches(matches), [
|
|
{ pattern: 0, captures: [{ name: "element", text: "d" }] },
|
|
{ pattern: 0, captures: [{ name: "element", text: "e" }] },
|
|
{ pattern: 0, captures: [{ name: "element", text: "f" }] },
|
|
{ pattern: 0, captures: [{ name: "element", text: "g" }] },
|
|
]);
|
|
});
|
|
|
|
it("handles predicates that compare the text of capture to literal strings", () => {
|
|
tree = parser.parse(`
|
|
giraffe(1, 2, []);
|
|
helment([false]);
|
|
goat(false);
|
|
gross(3, []);
|
|
hiccup([]);
|
|
gaff(5);
|
|
`);
|
|
|
|
// Find all calls to functions beginning with 'g', where one argument
|
|
// is an array literal.
|
|
query = JavaScript.query(`
|
|
(call_expression
|
|
function: (identifier) @name
|
|
arguments: (arguments (array))
|
|
(#match? @name "^g"))
|
|
`);
|
|
|
|
const matches = query.matches(tree.rootNode);
|
|
assert.deepEqual(formatMatches(matches), [
|
|
{ pattern: 0, captures: [{name: "name", text: "giraffe" }] },
|
|
{ pattern: 0, captures: [{name: "name", text: "gross" }] },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe(".captures", () => {
|
|
it("returns all of the captures for the given query, in order", () => {
|
|
tree = parser.parse(`
|
|
a({
|
|
bc: function de() {
|
|
const fg = function hi() {}
|
|
},
|
|
jk: function lm() {
|
|
const no = function pq() {}
|
|
},
|
|
});
|
|
`);
|
|
query = JavaScript.query(`
|
|
(pair
|
|
key: _ @method.def
|
|
(function
|
|
name: (identifier) @method.alias))
|
|
|
|
(variable_declarator
|
|
name: _ @function.def
|
|
value: (function
|
|
name: (identifier) @function.alias))
|
|
|
|
":" @delimiter
|
|
"=" @operator
|
|
`);
|
|
|
|
const captures = query.captures(tree.rootNode);
|
|
assert.deepEqual(formatCaptures(captures), [
|
|
{ name: "method.def", text: "bc" },
|
|
{ name: "delimiter", text: ":" },
|
|
{ name: "method.alias", text: "de" },
|
|
{ name: "function.def", text: "fg" },
|
|
{ name: "operator", text: "=" },
|
|
{ name: "function.alias", text: "hi" },
|
|
{ name: "method.def", text: "jk" },
|
|
{ name: "delimiter", text: ":" },
|
|
{ name: "method.alias", text: "lm" },
|
|
{ name: "function.def", text: "no" },
|
|
{ name: "operator", text: "=" },
|
|
{ name: "function.alias", text: "pq" },
|
|
]);
|
|
});
|
|
|
|
it("handles conditions that compare the text of capture to literal strings", () => {
|
|
tree = parser.parse(`
|
|
lambda
|
|
panda
|
|
load
|
|
toad
|
|
const ab = require('./ab');
|
|
new Cd(EF);
|
|
`);
|
|
|
|
query = JavaScript.query(`
|
|
((identifier) @variable
|
|
(#not-match? @variable "^(lambda|load)$"))
|
|
|
|
((identifier) @function.builtin
|
|
(#eq? @function.builtin "require"))
|
|
|
|
((identifier) @constructor
|
|
(#match? @constructor "^[A-Z]"))
|
|
|
|
((identifier) @constant
|
|
(#match? @constant "^[A-Z]{2,}$"))
|
|
`);
|
|
|
|
const captures = query.captures(tree.rootNode);
|
|
assert.deepEqual(formatCaptures(captures), [
|
|
{ name: "variable", text: "panda" },
|
|
{ name: "variable", text: "toad" },
|
|
{ name: "variable", text: "ab" },
|
|
{ name: "variable", text: "require" },
|
|
{ name: "function.builtin", text: "require" },
|
|
{ name: "variable", text: "Cd" },
|
|
{ name: "constructor", text: "Cd" },
|
|
{ name: "variable", text: "EF" },
|
|
{ name: "constructor", text: "EF" },
|
|
{ name: "constant", text: "EF" },
|
|
]);
|
|
});
|
|
|
|
it("handles conditions that compare the text of capture to each other", () => {
|
|
tree = parser.parse(`
|
|
ab = abc + 1;
|
|
def = de + 1;
|
|
ghi = ghi + 1;
|
|
`);
|
|
|
|
query = JavaScript.query(`
|
|
(
|
|
(assignment_expression
|
|
left: (identifier) @id1
|
|
right: (binary_expression
|
|
left: (identifier) @id2))
|
|
(#eq? @id1 @id2)
|
|
)
|
|
`);
|
|
|
|
const captures = query.captures(tree.rootNode);
|
|
assert.deepEqual(formatCaptures(captures), [
|
|
{ name: "id1", text: "ghi" },
|
|
{ name: "id2", text: "ghi" },
|
|
]);
|
|
});
|
|
|
|
it("handles patterns with properties", () => {
|
|
tree = parser.parse(`a(b.c);`);
|
|
query = JavaScript.query(`
|
|
((call_expression (identifier) @func)
|
|
(#set! foo)
|
|
(#set! bar baz))
|
|
|
|
((property_identifier) @prop
|
|
(#is? foo)
|
|
(#is-not? bar baz))
|
|
`);
|
|
|
|
const captures = query.captures(tree.rootNode);
|
|
assert.deepEqual(formatCaptures(captures), [
|
|
{ name: "func", text: "a", setProperties: { foo: null, bar: "baz" } },
|
|
{
|
|
name: "prop",
|
|
text: "c",
|
|
assertedProperties: { foo: null },
|
|
refutedProperties: { bar: "baz" },
|
|
},
|
|
]);
|
|
assert.ok(!query.didExceedMatchLimit());
|
|
});
|
|
|
|
it("detects queries with too many permutations to track", () => {
|
|
tree = parser.parse(`
|
|
[
|
|
hello, hello, hello, hello, hello, hello, hello, hello, hello, hello,
|
|
hello, hello, hello, hello, hello, hello, hello, hello, hello, hello,
|
|
hello, hello, hello, hello, hello, hello, hello, hello, hello, hello,
|
|
hello, hello, hello, hello, hello, hello, hello, hello, hello, hello,
|
|
hello, hello, hello, hello, hello, hello, hello, hello, hello, hello,
|
|
];
|
|
`);
|
|
|
|
query = JavaScript.query(`
|
|
(array (identifier) @pre (identifier) @post)
|
|
`);
|
|
|
|
const captures = query.captures(tree.rootNode);
|
|
assert.ok(query.didExceedMatchLimit());
|
|
});
|
|
});
|
|
|
|
describe(".predicatesForPattern(index)", () => {
|
|
it("returns all of the predicates as objects", () => {
|
|
query = JavaScript.query(`
|
|
(
|
|
(binary_expression
|
|
left: (identifier) @a
|
|
right: (identifier) @b)
|
|
(#something? @a @b)
|
|
(#match? @a "c")
|
|
(#something-else? @a "A" @b "B")
|
|
)
|
|
|
|
((identifier) @c
|
|
(#hello! @c))
|
|
|
|
"if" @d
|
|
`);
|
|
|
|
assert.deepEqual(query.predicatesForPattern(0), [
|
|
{
|
|
operator: "something?",
|
|
operands: [
|
|
{ type: "capture", name: "a" },
|
|
{ type: "capture", name: "b" },
|
|
],
|
|
},
|
|
{
|
|
operator: "something-else?",
|
|
operands: [
|
|
{ type: "capture", name: "a" },
|
|
{ type: "string", value: "A" },
|
|
{ type: "capture", name: "b" },
|
|
{ type: "string", value: "B" },
|
|
],
|
|
},
|
|
]);
|
|
assert.deepEqual(query.predicatesForPattern(1), [
|
|
{
|
|
operator: "hello!",
|
|
operands: [{ type: "capture", name: "c" }],
|
|
},
|
|
]);
|
|
assert.deepEqual(query.predicatesForPattern(2), []);
|
|
});
|
|
});
|
|
});
|
|
|
|
function formatMatches(matches) {
|
|
return matches.map(({ pattern, captures }) => ({
|
|
pattern,
|
|
captures: formatCaptures(captures),
|
|
}));
|
|
}
|
|
|
|
function formatCaptures(captures) {
|
|
return captures.map((c) => {
|
|
const node = c.node;
|
|
delete c.node;
|
|
c.text = node.text;
|
|
return c;
|
|
});
|
|
}
|