local t = require('test.testutil') local n = require('test.functional.testnvim')() local clear = n.clear local dedent = t.dedent local eq = t.eq local insert = n.insert local exec_lua = n.exec_lua local pcall_err = t.pcall_err local api = n.api local fn = n.fn local function get_query_result(query_text) local cquery = vim.treesitter.query.parse('c', query_text) local parser = vim.treesitter.get_parser(0, 'c') local tree = parser:parse()[1] local 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 describe('treesitter query API', function() before_each(function() clear() exec_lua(function() vim.g.__ts_debug = 1 end) 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(function() return vim.treesitter.query.get('c', 'highlights').captures end) -- 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(function() local before = vim.api.nvim__stats().ts_query_parse_count collectgarbage('stop') for _ = 1, _n, 1 do vim.treesitter.query.parse('c', long_query, _n) end collectgarbage('restart') collectgarbage('collect') local after = vim.api.nvim__stats().ts_query_parse_count return after - before end) end eq(1, q(1)) -- cache is cleared by garbage collection even if valid "cquery" reference is kept around eq(1, q(100)) end) it('supports query and iter by capture (iter_captures)', function() insert(test_text) local res = exec_lua(function() local cquery = vim.treesitter.query.parse('c', test_query) local parser = vim.treesitter.get_parser(0, 'c') local tree = parser:parse()[1] local 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 end) 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(function() local cquery = vim.treesitter.query.parse('c', test_query) local parser = vim.treesitter.get_parser(0, 'c') local tree = parser:parse()[1] local res = {} for pattern, match in cquery:iter_matches(tree:root(), 0, 7, 14) 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 end) 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(function() local cquery = vim.treesitter.query.parse( 'c', '(expression_statement (assignment_expression (call_expression)))+ @funccall' ) local parser = vim.treesitter.get_parser(0, 'c') local tree = parser:parse()[1] local 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 end) 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(function() local cquery = vim.treesitter.query.parse( 'c', '(expression_statement (assignment_expression (call_expression)))+ @funccall' ) local parser = vim.treesitter.get_parser(0, 'c') local tree = parser:parse()[1] local res = {} for pattern, match in cquery:iter_matches(tree:root(), 0, 7, 14) 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 end, '(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('returns quantified matches in order of range #29344', function() insert([[ int main() { int a, b, c, d, e, f, g, h, i; a = MIN(0, 1); b = MIN(0, 1); c = MIN(0, 1); d = MIN(0, 1); e = MIN(0, 1); f = MIN(0, 1); g = MIN(0, 1); h = MIN(0, 1); i = MIN(0, 1); } ]]) local res = exec_lua(function() local cquery = vim.treesitter.query.parse( 'c', '(expression_statement (assignment_expression (call_expression)))+ @funccall' ) local parser = vim.treesitter.get_parser(0, 'c') local tree = parser:parse()[1] local res = {} for pattern, match in cquery:iter_matches(tree:root(), 0, 7, 14) 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 end) eq({ { 1, { { '@funccall', 'expression_statement', 2, 2, 2, 16 }, { '@funccall', 'expression_statement', 3, 2, 3, 16 }, { '@funccall', 'expression_statement', 4, 2, 4, 16 }, { '@funccall', 'expression_statement', 5, 2, 5, 16 }, { '@funccall', 'expression_statement', 6, 2, 6, 16 }, { '@funccall', 'expression_statement', 7, 2, 7, 16 }, { '@funccall', 'expression_statement', 8, 2, 8, 16 }, { '@funccall', 'expression_statement', 9, 2, 9, 16 }, { '@funccall', 'expression_statement', 10, 2, 10, 16 }, }, }, }, 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(function() local query = ( '([_] @plus (#vim-match? @plus "^\\\\+$"))' .. '([_] @times (#vim-match? @times "^\\\\*$"))' .. '([_] @paren (#vim-match? @paren "^\\\\($"))' .. '([_] @escape (#vim-match? @escape "^\\\\\\\\n$"))' .. '([_] @string (#vim-match? @string "^\\"\\\\\\\\n\\"$"))' ) local cquery = vim.treesitter.query.parse('c', query) local parser = vim.treesitter.get_parser(0, 'c') local tree = parser:parse()[1] local res = {} for pattern, match in cquery:iter_matches(tree:root(), 0, 0, -1) 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 end) 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 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; } ]]) local res0 = exec_lua( 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( 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('supports builtin predicate has-ancestor?', function() insert([[ int x = 123; enum C { y = 124 }; int main() { int z = 125; }]]) local result = exec_lua( get_query_result, [[((number_literal) @literal (#has-ancestor? @literal "function_definition"))]] ) eq({ { 'literal', 'number_literal', { 2, 21, 2, 24 }, '125' } }, result) result = exec_lua( get_query_result, [[((number_literal) @literal (#has-ancestor? @literal "function_definition" "enum_specifier"))]] ) eq({ { 'literal', 'number_literal', { 1, 13, 1, 16 }, '124' }, { 'literal', 'number_literal', { 2, 21, 2, 24 }, '125' }, }, result) result = exec_lua( get_query_result, [[((number_literal) @literal (#not-has-ancestor? @literal "enum_specifier"))]] ) eq({ { 'literal', 'number_literal', { 0, 8, 0, 11 }, '123' }, { 'literal', 'number_literal', { 2, 21, 2, 24 }, '125' }, }, result) result = exec_lua( get_query_result, [[((number_literal) @literal (#has-ancestor? @literal "enumerator"))]] ) eq({ { 'literal', 'number_literal', { 1, 13, 1, 16 }, '124' }, }, result) result = exec_lua( get_query_result, [[((number_literal) @literal (#has-ancestor? @literal "number_literal"))]] ) eq({}, result) end) it('allows loading query with escaped quotes and capture them `#{lua,vim}-match`?', function() insert('char* astring = "Hello World!";') local res = exec_lua(function() local cquery = vim.treesitter.query.parse( 'c', '([_] @quote (#vim-match? @quote "^\\"$")) ([_] @quote (#lua-match? @quote "^\\"$"))' ) local parser = vim.treesitter.get_parser(0, 'c') local tree = parser:parse()[1] local res = {} for pattern, match in cquery:iter_matches(tree:root(), 0, 0, -1) 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 end) 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(function() 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') query.add_predicate('is-main?', is_main) local query0 = query.parse('c', custom_query) local nodes = {} for _, node in query0:iter_captures(parser:parse()[1]:root(), 0) do table.insert(nodes, { node:range() }) end return nodes end) 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(function() 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, { all = false, force = true }) local query0 = query.parse('c', custom_query) local nodes = {} for _, node in query0:iter_captures(parser:parse()[1]:root(), 0) do table.insert(nodes, { node:range() }) end return nodes end) -- 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(function() local query = vim.treesitter.query local r = {} for _, v in ipairs(query.list_predicates()) do r[v] = true end return r end) 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(function() local parser = vim.treesitter.get_parser(0, 'lua') local query0 = vim.treesitter.query.parse('lua', query) local nodes = {} for _, node in query0:iter_captures(parser:parse()[1]:root(), 0) do nodes[#nodes + 1] = { node:range() } end return nodes end) 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 result = exec_lua(function() local parser = vim.treesitter.get_parser(0, 'lua') local query = vim.treesitter.query.parse( 'lua', [[ (((comment (comment_content))+) @bar (#lua-match? @bar "Comment")) ]] ) local nodes = {} for _, node in query:iter_captures(parser:parse()[1]:root(), 0) do nodes[#nodes + 1] = { node:range() } end return nodes end) 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(function() local cquery = vim.treesitter.query.parse('c', test_query) local parser = vim.treesitter.get_parser(0, 'c') local tree = parser:parse()[1] local res = {} for pattern, match in cquery:iter_matches(tree:root(), 0, 7, 14, { all = false }) 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 end) 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(function() local parser = vim.treesitter.get_string_parser(txt, 'c') local nodes = {} local query = vim.treesitter.query.parse('c', '((identifier) @foo)') local first_child = assert(parser:parse()[1]:root():child(1)) for _, node in query:iter_captures(first_child, txt) do table.insert(nodes, { node:range() }) end return nodes end) 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 types test( '.../query.lua:0: Query error at 1:2. Invalid node type ">\\">>":\n' .. '">\\">>" @operator\n' .. ' ^', '">\\">>" @operator' ) test( '.../query.lua:0: Query error at 1:2. Invalid node type "\\\\":\n' .. '"\\\\" @operator\n' .. ' ^', '"\\\\" @operator' ) test( '.../query.lua:0: Query error at 1:2. Invalid node type ">>>":\n' .. '">>>" @operator\n' .. ' ^', '">>>" @operator' ) 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) describe('Query:iter_captures', function() it('includes metadata for all captured nodes #23664', function() insert([[ const char *sql = "SELECT * FROM Students WHERE name = 'Robert'); DROP TABLE Students;--"; ]]) local result = exec_lua(function() local query = vim.treesitter.query.parse( 'c', [[ (declaration type: (_) declarator: (init_declarator declarator: (pointer_declarator declarator: (identifier)) @_id value: (string_literal (string_content) @injection.content)) (#set! injection.language "sql") (#contains? @_id "sql")) ]] ) local parser = vim.treesitter.get_parser(0, 'c') local root = parser:parse()[1]:root() local res = {} for id, _, metadata in query:iter_captures(root, 0) do res[query.captures[id]] = metadata end return res end) eq({ ['_id'] = { ['injection.language'] = 'sql' }, ['injection.content'] = { ['injection.language'] = 'sql' }, }, result) end) it('only evaluates predicates once per match', function() insert([[ void foo(int x, int y); ]]) local query = [[ (declaration type: (_) declarator: (function_declarator declarator: (identifier) @function.name parameters: (parameter_list (parameter_declaration type: (_) declarator: (identifier) @argument))) (#eq? @function.name "foo")) ]] local result = exec_lua(function() local query0 = vim.treesitter.query.parse('c', query) local match_preds = query0.match_preds local called = 0 function query0:match_preds(...) called = called + 1 return match_preds(self, ...) end local parser = vim.treesitter.get_parser(0, 'c') local root = parser:parse()[1]:root() local captures = {} for id in query0:iter_captures(root, 0) do captures[#captures + 1] = id end return { called, captures } end) eq({ 2, { 1, 1, 2, 2 } }, result) end) end) end)