aboutsummaryrefslogtreecommitdiff
path: root/test/functional/treesitter/query_spec.lua
diff options
context:
space:
mode:
Diffstat (limited to 'test/functional/treesitter/query_spec.lua')
-rw-r--r--test/functional/treesitter/query_spec.lua699
1 files changed, 699 insertions, 0 deletions
diff --git a/test/functional/treesitter/query_spec.lua b/test/functional/treesitter/query_spec.lua
new file mode 100644
index 0000000000..fb3eaa1518
--- /dev/null
+++ b/test/functional/treesitter/query_spec.lua
@@ -0,0 +1,699 @@
+local helpers = require('test.functional.helpers')(after_each)
+
+local clear = helpers.clear
+local dedent = helpers.dedent
+local eq = helpers.eq
+local insert = helpers.insert
+local exec_lua = helpers.exec_lua
+local pcall_err = helpers.pcall_err
+local is_os = helpers.is_os
+local api = helpers.api
+local fn = helpers.fn
+
+describe('treesitter query API', function()
+ before_each(function()
+ clear()
+ exec_lua [[
+ vim.g.__ts_debug = 1
+ ]]
+ end)
+
+ local test_text = [[
+void ui_refresh(void)
+{
+ int width = INT_MAX, height = INT_MAX;
+ bool ext_widgets[kUIExtCount];
+ for (UIExtension i = 0; (int)i < kUIExtCount; i++) {
+ ext_widgets[i] = true;
+ }
+
+ bool inclusive = ui_override();
+ for (size_t i = 0; i < ui_count; i++) {
+ UI *ui = uis[i];
+ width = MIN(ui->width, width);
+ height = MIN(ui->height, height);
+ foo = BAR(ui->bazaar, bazaar);
+ for (UIExtension j = 0; (int)j < kUIExtCount; j++) {
+ ext_widgets[j] &= (ui->ui_ext[j] || inclusive);
+ }
+ }
+}]]
+
+ local test_query = [[
+ ((call_expression
+ function: (identifier) @minfunc
+ (argument_list (identifier) @min_id))
+ (#eq? @minfunc "MIN")
+ )
+
+ "for" @keyword
+
+ (primitive_type) @type
+
+ (field_expression argument: (identifier) @fieldarg)
+ ]]
+
+ it('supports runtime queries', function()
+ ---@type string[]
+ local ret = exec_lua [[
+ return vim.treesitter.query.get("c", "highlights").captures
+ ]]
+
+ -- see $VIMRUNTIME/queries/c/highlights.scm
+ eq('variable', ret[1])
+ eq('keyword', ret[2])
+ end)
+
+ it('supports caching queries', function()
+ local long_query = test_query:rep(100)
+ ---@return number
+ local function q(n)
+ return exec_lua(
+ [[
+ local query, n = ...
+ local before = vim.uv.hrtime()
+ for i=1, n, 1 do
+ cquery = vim.treesitter.query.parse("c", ...)
+ end
+ local after = vim.uv.hrtime()
+ return after - before
+ ]],
+ long_query,
+ n
+ )
+ end
+
+ local firstrun = q(1)
+ local manyruns = q(100)
+
+ -- First run should be at least 200x slower than an 100 subsequent runs.
+ local factor = is_os('win') and 100 or 200
+ assert(
+ factor * manyruns < firstrun,
+ ('firstrun: %f ms, manyruns: %f ms'):format(firstrun / 1e6, manyruns / 1e6)
+ )
+ end)
+
+ it('supports query and iter by capture (iter_captures)', function()
+ insert(test_text)
+
+ local res = exec_lua(
+ [[
+ cquery = vim.treesitter.query.parse("c", ...)
+ parser = vim.treesitter.get_parser(0, "c")
+ tree = parser:parse()[1]
+ res = {}
+ for cid, node in cquery:iter_captures(tree:root(), 0, 7, 14) do
+ -- can't transmit node over RPC. just check the name and range
+ table.insert(res, { '@' .. cquery.captures[cid], node:type(), node:range() })
+ end
+ return res
+ ]],
+ test_query
+ )
+
+ eq({
+ { '@type', 'primitive_type', 8, 2, 8, 6 }, -- bool
+ { '@keyword', 'for', 9, 2, 9, 5 }, -- for
+ { '@type', 'primitive_type', 9, 7, 9, 13 }, -- size_t
+ { '@minfunc', 'identifier', 11, 12, 11, 15 }, -- "MIN"(ui->width, width);
+ { '@fieldarg', 'identifier', 11, 16, 11, 18 }, -- ui
+ { '@min_id', 'identifier', 11, 27, 11, 32 }, -- width
+ { '@minfunc', 'identifier', 12, 13, 12, 16 }, -- "MIN"(ui->height, height);
+ { '@fieldarg', 'identifier', 12, 17, 12, 19 }, -- ui
+ { '@min_id', 'identifier', 12, 29, 12, 35 }, -- height
+ { '@fieldarg', 'identifier', 13, 14, 13, 16 }, -- ui ; in BAR(..)
+ }, res)
+ end)
+
+ it('supports query and iter by match (iter_matches)', function()
+ insert(test_text)
+
+ ---@type table
+ local res = exec_lua(
+ [[
+ cquery = vim.treesitter.query.parse("c", ...)
+ parser = vim.treesitter.get_parser(0, "c")
+ tree = parser:parse()[1]
+ res = {}
+ for pattern, match in cquery:iter_matches(tree:root(), 0, 7, 14, { all = true }) do
+ -- can't transmit node over RPC. just check the name and range
+ local mrepr = {}
+ for cid, nodes in pairs(match) do
+ for _, node in ipairs(nodes) do
+ table.insert(mrepr, { '@' .. cquery.captures[cid], node:type(), node:range() })
+ end
+ end
+ table.insert(res, { pattern, mrepr })
+ end
+ return res
+ ]],
+ test_query
+ )
+
+ eq({
+ { 3, { { '@type', 'primitive_type', 8, 2, 8, 6 } } },
+ { 2, { { '@keyword', 'for', 9, 2, 9, 5 } } },
+ { 3, { { '@type', 'primitive_type', 9, 7, 9, 13 } } },
+ { 4, { { '@fieldarg', 'identifier', 11, 16, 11, 18 } } },
+ {
+ 1,
+ {
+ { '@minfunc', 'identifier', 11, 12, 11, 15 },
+ { '@min_id', 'identifier', 11, 27, 11, 32 },
+ },
+ },
+ { 4, { { '@fieldarg', 'identifier', 12, 17, 12, 19 } } },
+ {
+ 1,
+ {
+ { '@minfunc', 'identifier', 12, 13, 12, 16 },
+ { '@min_id', 'identifier', 12, 29, 12, 35 },
+ },
+ },
+ { 4, { { '@fieldarg', 'identifier', 13, 14, 13, 16 } } },
+ }, res)
+ end)
+
+ it('supports query and iter by capture for quantifiers', function()
+ insert(test_text)
+
+ local res = exec_lua(
+ [[
+ cquery = vim.treesitter.query.parse("c", ...)
+ parser = vim.treesitter.get_parser(0, "c")
+ tree = parser:parse()[1]
+ res = {}
+ for cid, node in cquery:iter_captures(tree:root(), 0, 7, 14) do
+ -- can't transmit node over RPC. just check the name and range
+ table.insert(res, { '@' .. cquery.captures[cid], node:type(), node:range() })
+ end
+ return res
+ ]],
+ '(expression_statement (assignment_expression (call_expression)))+ @funccall'
+ )
+
+ eq({
+ { '@funccall', 'expression_statement', 11, 4, 11, 34 },
+ { '@funccall', 'expression_statement', 12, 4, 12, 37 },
+ { '@funccall', 'expression_statement', 13, 4, 13, 34 },
+ }, res)
+ end)
+
+ it('supports query and iter by match for quantifiers', function()
+ insert(test_text)
+
+ local res = exec_lua(
+ [[
+ cquery = vim.treesitter.query.parse("c", ...)
+ parser = vim.treesitter.get_parser(0, "c")
+ tree = parser:parse()[1]
+ res = {}
+ for pattern, match in cquery:iter_matches(tree:root(), 0, 7, 14, { all = true }) do
+ -- can't transmit node over RPC. just check the name and range
+ local mrepr = {}
+ for cid, nodes in pairs(match) do
+ for _, node in ipairs(nodes) do
+ table.insert(mrepr, { '@' .. cquery.captures[cid], node:type(), node:range() })
+ end
+ end
+ table.insert(res, {pattern, mrepr})
+ end
+ return res
+ ]],
+ '(expression_statement (assignment_expression (call_expression)))+ @funccall'
+ )
+
+ eq({
+ {
+ 1,
+ {
+ { '@funccall', 'expression_statement', 11, 4, 11, 34 },
+ { '@funccall', 'expression_statement', 12, 4, 12, 37 },
+ { '@funccall', 'expression_statement', 13, 4, 13, 34 },
+ },
+ },
+ }, res)
+ end)
+
+ it('can match special regex characters like \\ * + ( with `vim-match?`', function()
+ insert('char* astring = "\\n"; (1 + 1) * 2 != 2;')
+
+ ---@type table
+ local res = exec_lua([[
+ query = (
+ '([_] @plus (#vim-match? @plus "^\\\\+$"))' ..
+ '([_] @times (#vim-match? @times "^\\\\*$"))' ..
+ '([_] @paren (#vim-match? @paren "^\\\\($"))' ..
+ '([_] @escape (#vim-match? @escape "^\\\\\\\\n$"))' ..
+ '([_] @string (#vim-match? @string "^\\"\\\\\\\\n\\"$"))'
+ )
+ cquery = vim.treesitter.query.parse("c", query)
+ parser = vim.treesitter.get_parser(0, "c")
+ tree = parser:parse()[1]
+ res = {}
+ for pattern, match in cquery:iter_matches(tree:root(), 0, 0, -1, { all = true }) do
+ -- can't transmit node over RPC. just check the name and range
+ local mrepr = {}
+ for cid, nodes in pairs(match) do
+ for _, node in ipairs(nodes) do
+ table.insert(mrepr, { '@' .. cquery.captures[cid], node:type(), node:range() })
+ end
+ end
+ table.insert(res, { pattern, mrepr })
+ end
+ return res
+ ]])
+
+ eq({
+ { 2, { { '@times', '*', 0, 4, 0, 5 } } },
+ { 5, { { '@string', 'string_literal', 0, 16, 0, 20 } } },
+ { 4, { { '@escape', 'escape_sequence', 0, 17, 0, 19 } } },
+ { 3, { { '@paren', '(', 0, 22, 0, 23 } } },
+ { 1, { { '@plus', '+', 0, 25, 0, 26 } } },
+ { 2, { { '@times', '*', 0, 30, 0, 31 } } },
+ }, res)
+ end)
+
+ it('supports builtin query predicate any-of?', function()
+ insert([[
+ #include <stdio.h>
+
+ int main(void) {
+ int i;
+ for(i=1; i<=100; i++) {
+ if(((i%3)||(i%5))== 0)
+ printf("number= %d FizzBuzz\n", i);
+ else if((i%3)==0)
+ printf("number= %d Fizz\n", i);
+ else if((i%5)==0)
+ printf("number= %d Buzz\n", i);
+ else
+ printf("number= %d\n",i);
+ }
+ return 0;
+ }
+ ]])
+ exec_lua([[
+ function get_query_result(query_text)
+ cquery = vim.treesitter.query.parse("c", query_text)
+ parser = vim.treesitter.get_parser(0, "c")
+ tree = parser:parse()[1]
+ res = {}
+ for cid, node in cquery:iter_captures(tree:root(), 0) do
+ -- can't transmit node over RPC. just check the name, range, and text
+ local text = vim.treesitter.get_node_text(node, 0)
+ local range = {node:range()}
+ table.insert(res, { cquery.captures[cid], node:type(), range, text })
+ end
+ return res
+ end
+ ]])
+
+ local res0 = exec_lua(
+ [[return get_query_result(...)]],
+ [[((primitive_type) @c-keyword (#any-of? @c-keyword "int" "float"))]]
+ )
+ eq({
+ { 'c-keyword', 'primitive_type', { 2, 2, 2, 5 }, 'int' },
+ { 'c-keyword', 'primitive_type', { 3, 4, 3, 7 }, 'int' },
+ }, res0)
+
+ local res1 = exec_lua(
+ [[return get_query_result(...)]],
+ [[
+ ((string_literal) @fizzbuzz-strings (#any-of? @fizzbuzz-strings
+ "\"number= %d FizzBuzz\\n\""
+ "\"number= %d Fizz\\n\""
+ "\"number= %d Buzz\\n\""
+ ))
+ ]]
+ )
+ eq({
+ { 'fizzbuzz-strings', 'string_literal', { 6, 15, 6, 38 }, '"number= %d FizzBuzz\\n"' },
+ { 'fizzbuzz-strings', 'string_literal', { 8, 15, 8, 34 }, '"number= %d Fizz\\n"' },
+ { 'fizzbuzz-strings', 'string_literal', { 10, 15, 10, 34 }, '"number= %d Buzz\\n"' },
+ }, res1)
+ end)
+
+ it('allows loading query with escaped quotes and capture them `#{lua,vim}-match`?', function()
+ insert('char* astring = "Hello World!";')
+
+ local res = exec_lua([[
+ cquery = vim.treesitter.query.parse("c", '([_] @quote (#vim-match? @quote "^\\"$")) ([_] @quote (#lua-match? @quote "^\\"$"))')
+ parser = vim.treesitter.get_parser(0, "c")
+ tree = parser:parse()[1]
+ res = {}
+ for pattern, match in cquery:iter_matches(tree:root(), 0, 0, -1, { all = true }) do
+ -- can't transmit node over RPC. just check the name and range
+ local mrepr = {}
+ for cid, nodes in pairs(match) do
+ for _, node in ipairs(nodes) do
+ table.insert(mrepr, { '@' .. cquery.captures[cid], node:type(), node:range() })
+ end
+ end
+ table.insert(res, { pattern, mrepr })
+ end
+ return res
+ ]])
+
+ eq({
+ { 1, { { '@quote', '"', 0, 16, 0, 17 } } },
+ { 2, { { '@quote', '"', 0, 16, 0, 17 } } },
+ { 1, { { '@quote', '"', 0, 29, 0, 30 } } },
+ { 2, { { '@quote', '"', 0, 29, 0, 30 } } },
+ }, res)
+ end)
+
+ it('allows to add predicates', function()
+ insert([[
+ int main(void) {
+ return 0;
+ }
+ ]])
+
+ local custom_query = '((identifier) @main (#is-main? @main))'
+
+ do
+ local res = exec_lua(
+ [[
+ local query = vim.treesitter.query
+
+ local function is_main(match, pattern, bufnr, predicate)
+ local nodes = match[ predicate[2] ]
+ for _, node in ipairs(nodes) do
+ if vim.treesitter.get_node_text(node, bufnr) == 'main' then
+ return true
+ end
+ end
+ return false
+ end
+
+ local parser = vim.treesitter.get_parser(0, "c")
+
+ -- Time bomb: update this in 0.12
+ if vim.fn.has('nvim-0.12') == 1 then
+ return 'Update this test to remove this message and { all = true } from add_predicate'
+ end
+ query.add_predicate("is-main?", is_main, { all = true })
+
+ local query = query.parse("c", ...)
+
+ local nodes = {}
+ for _, node in query:iter_captures(parser:parse()[1]:root(), 0) do
+ table.insert(nodes, {node:range()})
+ end
+
+ return nodes
+ ]],
+ custom_query
+ )
+
+ eq({ { 0, 4, 0, 8 } }, res)
+ end
+
+ -- Once with the old API. Remove this whole 'do' block in 0.12
+ do
+ local res = exec_lua(
+ [[
+ local query = vim.treesitter.query
+
+ local function is_main(match, pattern, bufnr, predicate)
+ local node = match[ predicate[2] ]
+
+ return vim.treesitter.get_node_text(node, bufnr) == 'main'
+ end
+
+ local parser = vim.treesitter.get_parser(0, "c")
+
+ query.add_predicate("is-main?", is_main, true)
+
+ local query = query.parse("c", ...)
+
+ local nodes = {}
+ for _, node in query:iter_captures(parser:parse()[1]:root(), 0) do
+ table.insert(nodes, {node:range()})
+ end
+
+ return nodes
+ ]],
+ custom_query
+ )
+
+ -- Remove this 'do' block in 0.12
+ eq(0, fn.has('nvim-0.12'))
+ eq({ { 0, 4, 0, 8 } }, res)
+ end
+
+ do
+ local res = exec_lua [[
+ local query = vim.treesitter.query
+
+ local t = {}
+ for _, v in ipairs(query.list_predicates()) do
+ t[v] = true
+ end
+
+ return t
+ ]]
+
+ eq(true, res['is-main?'])
+ end
+ end)
+
+ it('supports "all" and "any" semantics for predicates on quantified captures #24738', function()
+ local query_all = [[
+ (((comment (comment_content))+) @bar
+ (#lua-match? @bar "Yes"))
+ ]]
+
+ local query_any = [[
+ (((comment (comment_content))+) @bar
+ (#any-lua-match? @bar "Yes"))
+ ]]
+
+ local function test(input, query)
+ api.nvim_buf_set_lines(0, 0, -1, true, vim.split(dedent(input), '\n'))
+ return exec_lua(
+ [[
+ local parser = vim.treesitter.get_parser(0, "lua")
+ local query = vim.treesitter.query.parse("lua", ...)
+ local nodes = {}
+ for _, node in query:iter_captures(parser:parse()[1]:root(), 0) do
+ nodes[#nodes+1] = { node:range() }
+ end
+ return nodes
+ ]],
+ query
+ )
+ end
+
+ eq(
+ {},
+ test(
+ [[
+ -- Yes
+ -- No
+ -- Yes
+ ]],
+ query_all
+ )
+ )
+
+ eq(
+ {
+ { 0, 2, 0, 8 },
+ { 1, 2, 1, 8 },
+ { 2, 2, 2, 8 },
+ },
+ test(
+ [[
+ -- Yes
+ -- Yes
+ -- Yes
+ ]],
+ query_all
+ )
+ )
+
+ eq(
+ {},
+ test(
+ [[
+ -- No
+ -- No
+ -- No
+ ]],
+ query_any
+ )
+ )
+
+ eq(
+ {
+ { 0, 2, 0, 7 },
+ { 1, 2, 1, 8 },
+ { 2, 2, 2, 7 },
+ },
+ test(
+ [[
+ -- No
+ -- Yes
+ -- No
+ ]],
+ query_any
+ )
+ )
+ end)
+
+ it('supports any- prefix to match any capture when using quantifiers #24738', function()
+ insert([[
+ -- Comment
+ -- Comment
+ -- Comment
+ ]])
+
+ local query = [[
+ (((comment (comment_content))+) @bar
+ (#lua-match? @bar "Comment"))
+ ]]
+
+ local result = exec_lua(
+ [[
+ local parser = vim.treesitter.get_parser(0, "lua")
+ local query = vim.treesitter.query.parse("lua", ...)
+ local nodes = {}
+ for _, node in query:iter_captures(parser:parse()[1]:root(), 0) do
+ nodes[#nodes+1] = { node:range() }
+ end
+ return nodes
+ ]],
+ query
+ )
+
+ eq({
+ { 0, 2, 0, 12 },
+ { 1, 2, 1, 12 },
+ { 2, 2, 2, 12 },
+ }, result)
+ end)
+
+ it('supports the old broken version of iter_matches #24738', function()
+ -- Delete this test in 0.12 when iter_matches is removed
+ eq(0, fn.has('nvim-0.12'))
+
+ insert(test_text)
+ local res = exec_lua(
+ [[
+ cquery = vim.treesitter.query.parse("c", ...)
+ parser = vim.treesitter.get_parser(0, "c")
+ tree = parser:parse()[1]
+ res = {}
+ for pattern, match in cquery:iter_matches(tree:root(), 0, 7, 14) do
+ local mrepr = {}
+ for cid, node in pairs(match) do
+ table.insert(mrepr, { '@' .. cquery.captures[cid], node:type(), node:range() })
+ end
+ table.insert(res, { pattern, mrepr })
+ end
+ return res
+ ]],
+ test_query
+ )
+
+ eq({
+ { 3, { { '@type', 'primitive_type', 8, 2, 8, 6 } } },
+ { 2, { { '@keyword', 'for', 9, 2, 9, 5 } } },
+ { 3, { { '@type', 'primitive_type', 9, 7, 9, 13 } } },
+ { 4, { { '@fieldarg', 'identifier', 11, 16, 11, 18 } } },
+ {
+ 1,
+ {
+ { '@minfunc', 'identifier', 11, 12, 11, 15 },
+ { '@min_id', 'identifier', 11, 27, 11, 32 },
+ },
+ },
+ { 4, { { '@fieldarg', 'identifier', 12, 17, 12, 19 } } },
+ {
+ 1,
+ {
+ { '@minfunc', 'identifier', 12, 13, 12, 16 },
+ { '@min_id', 'identifier', 12, 29, 12, 35 },
+ },
+ },
+ { 4, { { '@fieldarg', 'identifier', 13, 14, 13, 16 } } },
+ }, res)
+ end)
+
+ it('should use node range when omitted', function()
+ local txt = [[
+ int foo = 42;
+ int bar = 13;
+ ]]
+
+ local ret = exec_lua(
+ [[
+ local str = ...
+ local parser = vim.treesitter.get_string_parser(str, "c")
+
+ local nodes = {}
+ local query = vim.treesitter.query.parse("c", '((identifier) @foo)')
+ local first_child = parser:parse()[1]:root():child(1)
+
+ for _, node in query:iter_captures(first_child, str) do
+ table.insert(nodes, { node:range() })
+ end
+
+ return nodes
+ ]],
+ txt
+ )
+
+ eq({ { 1, 10, 1, 13 } }, ret)
+ end)
+
+ it('fails to load queries', function()
+ local function test(exp, cquery)
+ eq(exp, pcall_err(exec_lua, "vim.treesitter.query.parse('c', ...)", cquery))
+ end
+
+ -- Invalid node type
+ test(
+ '.../query.lua:0: Query error at 1:2. Invalid node type "dentifier":\n'
+ .. '(dentifier) @variable\n'
+ .. ' ^',
+ '(dentifier) @variable'
+ )
+
+ -- Impossible pattern
+ test(
+ '.../query.lua:0: Query error at 1:13. Impossible pattern:\n'
+ .. '(identifier (identifier) @variable)\n'
+ .. ' ^',
+ '(identifier (identifier) @variable)'
+ )
+
+ -- Invalid syntax
+ test(
+ '.../query.lua:0: Query error at 1:13. Invalid syntax:\n'
+ .. '(identifier @variable\n'
+ .. ' ^',
+ '(identifier @variable'
+ )
+
+ -- Invalid field name
+ test(
+ '.../query.lua:0: Query error at 1:15. Invalid field name "invalid_field":\n'
+ .. '((identifier) invalid_field: (identifier))\n'
+ .. ' ^',
+ '((identifier) invalid_field: (identifier))'
+ )
+
+ -- Invalid capture name
+ test(
+ '.../query.lua:0: Query error at 3:2. Invalid capture name "ok.capture":\n'
+ .. '@ok.capture\n'
+ .. ' ^',
+ '((identifier) @id \n(#eq? @id\n@ok.capture\n))'
+ )
+ end)
+end)