tree-sitter/lib/binding_web/test/query-test.js

482 lines
14 KiB
JavaScript
Raw Normal View History

const {assert} = require('chai');
let Parser; let JavaScript;
describe('Query', () => {
let parser; let tree; let 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))\'');
});
2019-09-16 10:25:44 -07:00
it('throws an error on invalid predicates', () => {
2019-09-16 10:25:44 -07:00
assert.throws(() => {
JavaScript.query('((identifier) @abc (#eq? @ab hi))');
}, 'Bad capture name @ab');
2019-09-16 10:25:44 -07:00
assert.throws(() => {
JavaScript.query('((identifier) @abc (#eq? @ab hi))');
}, 'Bad capture name @ab');
2019-09-16 10:25:44 -07:00
assert.throws(() => {
JavaScript.query('((identifier) @abc (#eq?))');
}, 'Wrong number of arguments to `#eq?` predicate. Expected 2, got 0');
2019-09-16 10:25:44 -07:00
assert.throws(() => {
JavaScript.query('((identifier) @a (#eq? @a @a @a))');
}, 'Wrong number of arguments to `#eq?` predicate. Expected 2, got 3');
2019-09-16 10:25:44 -07:00
});
});
describe('.matches', () => {
it('returns all of the matches for the given query', () => {
tree = parser.parse('function one() { two(); function three() {} }');
query = JavaScript.query(`
2020-05-11 15:22:05 -07:00
(function_declaration name: (identifier) @fn-def)
(call_expression function: (identifier) @fn-ref)
`);
const matches = query.matches(tree.rootNode);
2020-05-11 15:22:05 -07:00
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'}]},
2020-05-11 15:22:05 -07:00
]);
});
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,
{
startPosition: {row: 1, column: 1},
endPosition: {row: 3, column: 1},
},
);
2020-05-11 15:22:05 -07:00
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'}]},
2020-05-11 15:22:05 -07:00
]);
});
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'}]},
]);
});
it('handles multiple matches where the first one is filtered', () => {
tree = parser.parse(`
const a = window.b;
`);
query = JavaScript.query(`
((identifier) @variable.builtin
(#match? @variable.builtin "^(arguments|module|console|window|document)$")
(#is-not? local))
`);
const matches = query.matches(tree.rootNode);
assert.deepEqual(formatMatches(matches), [
{pattern: 0, captures: [{name: 'variable.builtin', text: 'window'}]},
]);
});
});
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
2020-05-11 15:22:05 -07:00
key: _ @method.def
2024-02-02 11:30:19 -05:00
(function_expression
name: (identifier) @method.alias))
(variable_declarator
2020-05-11 15:22:05 -07:00
name: _ @function.def
2024-02-02 11:30:19 -05:00
value: (function_expression
name: (identifier) @function.alias))
":" @delimiter
"=" @operator
`);
const captures = query.captures(tree.rootNode);
2020-05-11 15:22:05 -07:00
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'},
2020-05-11 15:22:05 -07:00
]);
});
2019-09-16 10:25:44 -07:00
it('handles conditions that compare the text of capture to literal strings', () => {
2019-09-16 10:25:44 -07:00
tree = parser.parse(`
lambda
panda
load
toad
2019-09-16 10:25:44 -07:00
const ab = require('./ab');
new Cd(EF);
`);
query = JavaScript.query(`
((identifier) @variable
(#not-match? @variable "^(lambda|load)$"))
2019-09-16 10:25:44 -07:00
((identifier) @function.builtin
2020-05-11 15:22:05 -07:00
(#eq? @function.builtin "require"))
2019-09-16 10:25:44 -07:00
((identifier) @constructor
2020-05-11 15:22:05 -07:00
(#match? @constructor "^[A-Z]"))
2019-09-16 10:25:44 -07:00
((identifier) @constant
2020-05-11 15:22:05 -07:00
(#match? @constant "^[A-Z]{2,}$"))
2019-09-16 10:25:44 -07:00
`);
const captures = query.captures(tree.rootNode);
2020-05-11 15:22:05 -07:00
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'},
2020-05-11 15:22:05 -07:00
]);
2019-09-16 10:25:44 -07:00
});
it('handles conditions that compare the text of capture to each other', () => {
2019-09-16 10:25:44 -07:00
tree = parser.parse(`
ab = abc + 1;
def = de + 1;
ghi = ghi + 1;
2019-09-16 10:25:44 -07:00
`);
query = JavaScript.query(`
2020-05-11 15:22:05 -07:00
(
(assignment_expression
left: (identifier) @id1
right: (binary_expression
2019-09-16 10:25:44 -07:00
left: (identifier) @id2))
2020-05-11 15:22:05 -07:00
(#eq? @id1 @id2)
)
2019-09-16 10:25:44 -07:00
`);
const captures = query.captures(tree.rootNode);
2020-05-11 15:22:05 -07:00
assert.deepEqual(formatCaptures(captures), [
{name: 'id1', text: 'ghi'},
{name: 'id2', text: 'ghi'},
2020-05-11 15:22:05 -07:00
]);
2019-09-16 10:25:44 -07:00
});
it('handles patterns with properties', () => {
tree = parser.parse(`a(b.c);`);
query = JavaScript.query(`
((call_expression (identifier) @func)
2020-05-11 15:22:05 -07:00
(#set! foo)
(#set! bar baz))
((property_identifier) @prop
2020-05-11 15:22:05 -07:00
(#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'}},
2020-05-11 15:22:05 -07:00
{
name: 'prop',
text: 'c',
assertedProperties: {foo: null},
refutedProperties: {bar: 'baz'},
2020-05-11 15:22:05 -07:00
},
]);
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)
`);
query.captures(tree.rootNode, {matchLimit: 32});
assert.ok(query.didExceedMatchLimit());
});
it('handles quantified captures properly', () => {
let captures;
tree = parser.parse(`
/// foo
/// bar
/// baz
`);
query = JavaScript.query(`
(
(comment)+ @foo
(#any-eq? @foo "/// foo")
)
`);
const expectCount = (tree, queryText, expectedCount) => {
query = JavaScript.query(queryText);
captures = query.captures(tree.rootNode);
assert.equal(captures.length, expectedCount);
};
expectCount(
tree,
`((comment)+ @foo (#any-eq? @foo "/// foo"))`,
3,
);
expectCount(
tree,
`((comment)+ @foo (#eq? @foo "/// foo"))`,
0,
);
expectCount(
tree,
`((comment)+ @foo (#any-not-eq? @foo "/// foo"))`,
3,
);
expectCount(
tree,
`((comment)+ @foo (#not-eq? @foo "/// foo"))`,
0,
);
expectCount(
tree,
`((comment)+ @foo (#match? @foo "^/// foo"))`,
0,
);
expectCount(
tree,
`((comment)+ @foo (#any-match? @foo "^/// foo"))`,
3,
);
expectCount(
tree,
`((comment)+ @foo (#not-match? @foo "^/// foo"))`,
0,
);
expectCount(
tree,
`((comment)+ @foo (#not-match? @foo "fsdfsdafdfs"))`,
3,
);
expectCount(
tree,
`((comment)+ @foo (#any-not-match? @foo "^///"))`,
0,
);
expectCount(
tree,
`((comment)+ @foo (#any-not-match? @foo "^/// foo"))`,
3,
);
});
});
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), []);
});
});
describe('.disableCapture', () => {
it('disables a capture', () => {
const query = JavaScript.query(`
(function_declaration
(identifier) @name1 @name2 @name3
(statement_block) @body1 @body2)
`);
const source = 'function foo() { return 1; }';
const tree = parser.parse(source);
let matches = query.matches(tree.rootNode);
assert.deepEqual(formatMatches(matches), [
{
pattern: 0,
captures: [
{name: 'name1', text: 'foo'},
{name: 'name2', text: 'foo'},
{name: 'name3', text: 'foo'},
{name: 'body1', text: '{ return 1; }'},
{name: 'body2', text: '{ return 1; }'},
],
},
]);
// disabling captures still works when there are multiple captures on a
// single node.
query.disableCapture('name2');
matches = query.matches(tree.rootNode);
assert.deepEqual(formatMatches(matches), [
{
pattern: 0,
captures: [
{name: 'name1', text: 'foo'},
{name: 'name3', text: 'foo'},
{name: 'body1', text: '{ return 1; }'},
{name: 'body2', text: '{ return 1; }'},
],
},
]);
});
});
describe('Set a timeout', () =>
it('returns less than the expected matches', () => {
tree = parser.parse('function foo() while (true) { } }\n'.repeat(1000));
query = JavaScript.query('(function_declaration name: (identifier) @function)');
const matches = query.matches(tree.rootNode, { timeoutMicros: 1000 });
assert.isBelow(matches.length, 1000);
const matches2 = query.matches(tree.rootNode, { timeoutMicros: 0 });
assert.equal(matches2.length, 1000);
})
);
});
function formatMatches(matches) {
return matches.map(({pattern, captures}) => ({
pattern,
2020-05-11 15:22:05 -07:00
captures: formatCaptures(captures),
}));
}
function formatCaptures(captures) {
2020-05-11 15:22:05 -07:00
return captures.map((c) => {
const node = c.node;
delete c.node;
c.text = node.text;
return c;
2020-05-11 15:22:05 -07:00
});
}