HEX
Server: Apache/2.4.41 (Ubuntu)
System: Linux ip-172-31-42-149 5.15.0-1084-aws #91~20.04.1-Ubuntu SMP Fri May 2 07:00:04 UTC 2025 aarch64
User: ubuntu (1000)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //home/ubuntu/neovim/test/functional/treesitter/query_spec.lua
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(query, n0)
        local before = vim.api.nvim__stats().ts_query_parse_count
        collectgarbage('stop')
        for _ = 1, n0, 1 do
          vim.treesitter.query.parse('c', query, n0)
        end
        collectgarbage('restart')
        collectgarbage('collect')
        local after = vim.api.nvim__stats().ts_query_parse_count
        return after - before
      end, long_query, _n)
    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(test_query0)
      local cquery = vim.treesitter.query.parse('c', test_query0)
      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, 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(function(test_query0)
      local cquery = vim.treesitter.query.parse('c', test_query0)
      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 = 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
    end, 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(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, { 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
    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, { 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
    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, { 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
    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 <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;
      }
    ]])

    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)
  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, { 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
    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(custom_query0)
        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 query0 = query.parse('c', custom_query0)

        local nodes = {}
        for _, node in query0:iter_captures(parser:parse()[1]:root(), 0) do
          table.insert(nodes, { node:range() })
        end

        return nodes
      end, 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(function(custom_query0)
        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 query0 = query.parse('c', custom_query0)

        local nodes = {}
        for _, node in query0:iter_captures(parser:parse()[1]:root(), 0) do
          table.insert(nodes, { node:range() })
        end

        return nodes
      end, 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(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(query_str)
        local parser = vim.treesitter.get_parser(0, 'lua')
        local query0 = vim.treesitter.query.parse('lua', query_str)
        local nodes = {}
        for _, node in query0:iter_captures(parser:parse()[1]:root(), 0) do
          nodes[#nodes + 1] = { node:range() }
        end
        return nodes
      end, 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 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(test_query0)
      local cquery = vim.treesitter.query.parse('c', test_query0)
      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
        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, 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(function(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
    end, 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)

  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(query_str)
        local query0 = vim.treesitter.query.parse('c', query_str)
        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, query)

      eq({ 2, { 1, 1, 2, 2 } }, result)
    end)
  end)
end)