Merge pull request #28006 from wookayin/ts/refactor-tests

test(treesitter): refactor parser_spec, and separate query_spec
This commit is contained in:
Gregory Anders 2024-03-25 08:06:08 -05:00 committed by GitHub
commit 829080d8c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 801 additions and 717 deletions

View File

@ -7,9 +7,6 @@ local insert = helpers.insert
local exec_lua = helpers.exec_lua local exec_lua = helpers.exec_lua
local pcall_err = helpers.pcall_err local pcall_err = helpers.pcall_err
local feed = helpers.feed local feed = helpers.feed
local is_os = helpers.is_os
local api = helpers.api
local fn = helpers.fn
describe('treesitter parser API', function() describe('treesitter parser API', function()
before_each(function() before_each(function()
@ -121,7 +118,7 @@ void ui_refresh(void)
res = {} res = {}
for node, field in func_node:iter_children() do for node, field in func_node:iter_children() do
table.insert(res, {node:type(), field}) table.insert(res, { node:type(), field })
end end
return res return res
]]) ]])
@ -157,7 +154,7 @@ void ui_refresh(void)
local res = {} local res = {}
for _, node in ipairs(func_node:field("type")) do for _, node in ipairs(func_node:field("type")) do
table.insert(res, {node:type(), node:range()}) table.insert(res, { node:type(), node:range() })
end end
return res return res
]]) ]])
@ -173,185 +170,6 @@ void ui_refresh(void)
assert(res_fail) assert(res_fail)
end) end)
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()
local ret = exec_lua [[
return vim.treesitter.query.get("c", "highlights").captures[1]
]]
eq('variable', ret)
end)
it('supports caching queries', function()
local long_query = test_query:rep(100)
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('support query and iter by capture', 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 },
{ 'keyword', 'for', 9, 2, 9, 5 },
{ 'type', 'primitive_type', 9, 7, 9, 13 },
{ 'minfunc', 'identifier', 11, 12, 11, 15 },
{ 'fieldarg', 'identifier', 11, 16, 11, 18 },
{ 'min_id', 'identifier', 11, 27, 11, 32 },
{ 'minfunc', 'identifier', 12, 13, 12, 16 },
{ 'fieldarg', 'identifier', 12, 17, 12, 19 },
{ 'min_id', 'identifier', 12, 29, 12, 35 },
{ 'fieldarg', 'identifier', 13, 14, 13, 16 },
}, res)
end)
it('support query and iter by match', 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
]],
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('support 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('support 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('supports getting text of multiline node', function() it('supports getting text of multiline node', function()
insert(test_text) insert(test_text)
local res = exec_lua([[ local res = exec_lua([[
@ -369,7 +187,7 @@ void ui_refresh(void)
eq('void', res2) eq('void', res2)
end) end)
it('support getting text where start of node is one past EOF', function() it('supports getting text where start of node is one past EOF', function()
local text = [[ local text = [[
def run def run
a = <<~E a = <<~E
@ -378,25 +196,25 @@ end]]
eq( eq(
'', '',
exec_lua [[ exec_lua [[
local fake_node = {} local fake_node = {}
function fake_node:start() function fake_node:start()
return 3, 0, 23 return 3, 0, 23
end
function fake_node:end_()
return 3, 0, 23
end
function fake_node:range(bytes)
if bytes then
return 3, 0, 23, 3, 0, 23
end end
return 3, 0, 3, 0 function fake_node:end_()
end return 3, 0, 23
return vim.treesitter.get_node_text(fake_node, 0) end
]] function fake_node:range(bytes)
if bytes then
return 3, 0, 23, 3, 0, 23
end
return 3, 0, 3, 0
end
return vim.treesitter.get_node_text(fake_node, 0)
]]
) )
end) end)
it('support getting empty text if node range is zero width', function() it('supports getting empty text if node range is zero width', function()
local text = [[ local text = [[
```lua ```lua
{} {}
@ -418,387 +236,6 @@ end]]
eq(true, result) eq(true, result)
end) end)
it('can match special regex characters like \\ * + ( with `vim-match?`', function()
insert('char* astring = "\\n"; (1 + 1) * 2 != 2;')
local res = exec_lua([[
cquery = vim.treesitter.query.parse("c", '([_] @plus (#vim-match? @plus "^\\\\+$"))'..
'([_] @times (#vim-match? @times "^\\\\*$"))'..
'([_] @paren (#vim-match? @paren "^\\\\($"))'..
'([_] @escape (#vim-match? @escape "^\\\\\\\\n$"))'..
'([_] @string (#vim-match? @string "^\\"\\\\\\\\n\\"$"))')
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(
'allow loading query with escaped quotes and capture them with `lua-match?` and `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('allows to set simple ranges', function() it('allows to set simple ranges', function()
insert(test_text) insert(test_text)
@ -882,56 +319,31 @@ end]]
local ret = exec_lua( local ret = exec_lua(
[[ [[
local str = ... local str = ...
local parser = vim.treesitter.get_string_parser(str, "c") local parser = vim.treesitter.get_string_parser(str, "c")
local nodes = {} local nodes = {}
local query = vim.treesitter.query.parse("c", '((identifier) @id (#eq? @id "foo"))') local query = vim.treesitter.query.parse("c", '((identifier) @id (#eq? @id "foo"))')
for _, node in query:iter_captures(parser:parse()[1]:root(), str) do for _, node in query:iter_captures(parser:parse()[1]:root(), str) do
table.insert(nodes, { node:range() }) table.insert(nodes, { node:range() })
end end
return nodes]], return nodes
]],
txt txt
) )
eq({ { 0, 10, 0, 13 } }, ret) eq({ { 0, 10, 0, 13 } }, ret)
end) 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)
describe('when creating a language tree', function() describe('when creating a language tree', function()
local function get_ranges() local function get_ranges()
return exec_lua([[ return exec_lua [[
local result = {} local result = {}
parser:for_each_tree(function(tree) table.insert(result, {tree:root():range()}) end) parser:for_each_tree(function(tree) table.insert(result, {tree:root():range()}) end)
return result return result
]]) ]]
end end
before_each(function() before_each(function()
@ -948,10 +360,14 @@ int x = INT_MAX;
describe('when parsing regions independently', function() describe('when parsing regions independently', function()
it('should inject a language', function() it('should inject a language', function()
exec_lua([[ exec_lua([[
parser = vim.treesitter.get_parser(0, "c", { parser = vim.treesitter.get_parser(0, "c", {
injections = { injections = {
c = '(preproc_def (preproc_arg) @injection.content (#set! injection.language "c")) (preproc_function_def value: (preproc_arg) @injection.content (#set! injection.language "c"))'}}) c = (
parser:parse(true) '(preproc_def (preproc_arg) @injection.content (#set! injection.language "c")) ' ..
'(preproc_function_def value: (preproc_arg) @injection.content (#set! injection.language "c"))'
)
}})
parser:parse(true)
]]) ]])
eq('table', exec_lua('return type(parser:children().c)')) eq('table', exec_lua('return type(parser:children().c)'))
@ -981,10 +397,14 @@ int x = INT_MAX;
describe('when parsing regions combined', function() describe('when parsing regions combined', function()
it('should inject a language', function() it('should inject a language', function()
exec_lua([[ exec_lua([[
parser = vim.treesitter.get_parser(0, "c", { parser = vim.treesitter.get_parser(0, "c", {
injections = { injections = {
c = '(preproc_def (preproc_arg) @injection.content (#set! injection.language "c") (#set! injection.combined)) (preproc_function_def value: (preproc_arg) @injection.content (#set! injection.language "c") (#set! injection.combined))'}}) c = (
parser:parse(true) '(preproc_def (preproc_arg) @injection.content (#set! injection.language "c") (#set! injection.combined)) ' ..
'(preproc_function_def value: (preproc_arg) @injection.content (#set! injection.language "c") (#set! injection.combined))'
)
}})
parser:parse(true)
]]) ]])
eq('table', exec_lua('return type(parser:children().c)')) eq('table', exec_lua('return type(parser:children().c)'))
@ -1027,10 +447,14 @@ int x = INT_MAX;
describe('when using injection.self', function() describe('when using injection.self', function()
it('should inject the source language', function() it('should inject the source language', function()
exec_lua([[ exec_lua([[
parser = vim.treesitter.get_parser(0, "c", { parser = vim.treesitter.get_parser(0, "c", {
injections = { injections = {
c = '(preproc_def (preproc_arg) @injection.content (#set! injection.self)) (preproc_function_def value: (preproc_arg) @injection.content (#set! injection.self))'}}) c = (
parser:parse(true) '(preproc_def (preproc_arg) @injection.content (#set! injection.self)) ' ..
'(preproc_function_def value: (preproc_arg) @injection.content (#set! injection.self))'
)
}})
parser:parse(true)
]]) ]])
eq('table', exec_lua('return type(parser:children().c)')) eq('table', exec_lua('return type(parser:children().c)'))
@ -1060,10 +484,14 @@ int x = INT_MAX;
describe('when using the offset directive', function() describe('when using the offset directive', function()
it('should shift the range by the directive amount', function() it('should shift the range by the directive amount', function()
exec_lua([[ exec_lua([[
parser = vim.treesitter.get_parser(0, "c", { parser = vim.treesitter.get_parser(0, "c", {
injections = { injections = {
c = '(preproc_def ((preproc_arg) @injection.content (#set! injection.language "c") (#offset! @injection.content 0 2 0 -1))) (preproc_function_def value: (preproc_arg) @injection.content (#set! injection.language "c"))'}}) c = (
parser:parse(true) '(preproc_def ((preproc_arg) @injection.content (#set! injection.language "c") (#offset! @injection.content 0 2 0 -1))) ' ..
'(preproc_function_def value: (preproc_arg) @injection.content (#set! injection.language "c"))'
)
}})
parser:parse(true)
]]) ]])
eq('table', exec_lua('return type(parser:children().c)')) eq('table', exec_lua('return type(parser:children().c)'))
@ -1078,13 +506,13 @@ int x = INT_MAX;
end) end)
it('should list all directives', function() it('should list all directives', function()
local res_list = exec_lua [[ local res_list = exec_lua [[
local query = vim.treesitter.query local query = vim.treesitter.query
local list = query.list_directives() local list = query.list_directives()
table.sort(list) table.sort(list)
return list return list
]] ]]
eq({ 'gsub!', 'offset!', 'set!', 'trim!' }, res_list) eq({ 'gsub!', 'offset!', 'set!', 'trim!' }, res_list)
@ -1102,13 +530,16 @@ int x = INT_MAX;
it('should return the correct language tree', function() it('should return the correct language tree', function()
local result = exec_lua([[ local result = exec_lua([[
parser = vim.treesitter.get_parser(0, "c", { parser = vim.treesitter.get_parser(0, "c", {
injections = { c = '(preproc_def (preproc_arg) @injection.content (#set! injection.language "c"))'}}) injections = {
parser:parse(true) c = '(preproc_def (preproc_arg) @injection.content (#set! injection.language "c"))'
}
})
parser:parse(true)
local sub_tree = parser:language_for_range({1, 18, 1, 19}) local sub_tree = parser:language_for_range({1, 18, 1, 19})
return sub_tree == parser:children().c return sub_tree == parser:children().c
]]) ]])
eq(true, result) eq(true, result)
@ -1208,15 +639,15 @@ int x = INT_MAX;
local function run_query() local function run_query()
return exec_lua( return exec_lua(
[[ [[
local query = vim.treesitter.query.parse("c", ...) local query = vim.treesitter.query.parse("c", ...)
parser = vim.treesitter.get_parser() parser = vim.treesitter.get_parser()
tree = parser:parse()[1] tree = parser:parse()[1]
res = {} res = {}
for id, node in query:iter_captures(tree:root()) do for id, node in query:iter_captures(tree:root()) do
table.insert(res, {query.captures[id], node:range()}) table.insert(res, {query.captures[id], node:range()})
end end
return res return res
]], ]],
query0 query0
) )
end end
@ -1258,14 +689,14 @@ int x = INT_MAX;
local r = exec_lua( local r = exec_lua(
[[ [[
local parser = vim.treesitter.get_string_parser(..., 'lua') local parser = vim.treesitter.get_string_parser(..., 'lua')
parser:parse(true) parser:parse(true)
local ranges = {} local ranges = {}
parser:for_each_tree(function(tstree, tree) parser:for_each_tree(function(tstree, tree)
ranges[tree:lang()] = { tstree:root():range(true) } ranges[tree:lang()] = { tstree:root():range(true) }
end) end)
return ranges return ranges
]], ]],
source source
) )
@ -1281,14 +712,14 @@ int x = INT_MAX;
local rb = exec_lua( local rb = exec_lua(
[[ [[
local r, source = ... local r, source = ...
local add_bytes = require('vim.treesitter._range').add_bytes local add_bytes = require('vim.treesitter._range').add_bytes
for lang, range in pairs(r) do for lang, range in pairs(r) do
r[lang] = {range[1], range[2], range[4], range[5]} r[lang] = {range[1], range[2], range[4], range[5]}
r[lang] = add_bytes(source, r[lang]) r[lang] = add_bytes(source, r[lang])
end end
return r return r
]], ]],
r, r,
source source
) )
@ -1402,53 +833,7 @@ int x = INT_MAX;
) )
end) end)
it('fails to load queries', function() describe('languagetree is_valid()', 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)
describe('is_valid()', function()
before_each(function() before_each(function()
insert(dedent [[ insert(dedent [[
Treesitter integration *treesitter* Treesitter integration *treesitter*

View File

@ -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)