From c194acbfc479d8e5839fa629363f93f6550d035c Mon Sep 17 00:00:00 2001 From: Stephan Seitz Date: Sat, 29 Apr 2023 18:22:26 +0200 Subject: feat(treesitter): add query_linter from nvim-treesitter/playground (#22784) Co-authored-by: clason Co-authored-by: lewis6991 --- runtime/lua/vim/treesitter/_query_linter.lua | 302 +++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 runtime/lua/vim/treesitter/_query_linter.lua (limited to 'runtime/lua/vim/treesitter/_query_linter.lua') diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua new file mode 100644 index 0000000000..62f28d3097 --- /dev/null +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -0,0 +1,302 @@ +local namespace = vim.api.nvim_create_namespace('vim.treesitter.query_linter') +-- those node names exist for every language +local BUILT_IN_NODE_NAMES = { '_', 'ERROR' } + +local M = {} + +--- @class QueryLinterNormalizedOpts +--- @field langs string[] +--- @field clear boolean + +--- @private +--- Caches parse results for queries for each language. +--- Entries of parse_cache[lang][query_text] will either be true for successful parse or contain the +--- error message of the parse +--- @type table> +local parse_cache = {} + +--- Contains language dependent context for the query linter +--- @class QueryLinterLanguageContext +--- @field lang string? Current `lang` of the targeted parser +--- @field parser_info table? Parser info returned by vim.treesitter.language.inspect +--- @field is_first_lang boolean Whether this is the first language of a linter run checking queries for multiple `langs` + +--- @private +--- Adds a diagnostic for node in the query buffer +--- @param diagnostics Diagnostic[] +--- @param node TSNode +--- @param buf integer +--- @param lint string +--- @param lang string? +local function add_lint_for_node(diagnostics, node, buf, lint, lang) + local node_text = vim.treesitter.get_node_text(node, buf):gsub('\n', ' ') + --- @type string + local message = lint .. ': ' .. node_text + local error_range = { node:range() } + diagnostics[#diagnostics + 1] = { + lnum = error_range[1], + end_lnum = error_range[3], + col = error_range[2], + end_col = error_range[4], + severity = vim.diagnostic.ERROR, + message = message, + source = lang, + } +end + +--- @private +--- Determines the target language of a query file by its path: /.scm +--- @param buf integer +--- @return string? +local function guess_query_lang(buf) + local filename = vim.api.nvim_buf_get_name(buf) + if filename ~= '' then + local ok, query_lang = pcall(vim.fn.fnamemodify, filename, ':p:h:t') + if ok then + return query_lang + end + end +end + +--- @private +--- @param buf integer +--- @param opts QueryLinterOpts|QueryLinterNormalizedOpts|nil +--- @return QueryLinterNormalizedOpts +local function normalize_opts(buf, opts) + opts = opts or {} + if not opts.langs then + opts.langs = guess_query_lang(buf) + end + + if type(opts.langs) ~= 'table' then + --- @diagnostic disable-next-line:assign-type-mismatch + opts.langs = { opts.langs } + end + + --- @cast opts QueryLinterNormalizedOpts + opts.langs = opts.langs or {} + return opts +end + +local lint_query = [[;; query + (program [(named_node) (list) (grouping)] @toplevel) + (named_node + name: _ @node.named) + (anonymous_node + name: _ @node.anonymous) + (field_definition + name: (identifier) @field) + (predicate + name: (identifier) @predicate.name + type: (predicate_type) @predicate.type) + (ERROR) @error +]] + +--- @private +--- @param node TSNode +--- @param buf integer +--- @param lang string +--- @param diagnostics Diagnostic[] +local function check_toplevel(node, buf, lang, diagnostics) + local query_text = vim.treesitter.get_node_text(node, buf) + + if not parse_cache[lang] then + parse_cache[lang] = {} + end + + local lang_cache = parse_cache[lang] + + if lang_cache[query_text] == nil then + local ok, err = pcall(vim.treesitter.query.parse, lang, query_text) + + if not ok and type(err) == 'string' then + err = err:match('.-:%d+: (.+)') + end + + lang_cache[query_text] = ok or err + end + + local cache_entry = lang_cache[query_text] + + if type(cache_entry) == 'string' then + add_lint_for_node(diagnostics, node, buf, cache_entry, lang) + end +end + +--- @private +--- @param node TSNode +--- @param buf integer +--- @param lang string +--- @param parser_info table +--- @param diagnostics Diagnostic[] +local function check_field(node, buf, lang, parser_info, diagnostics) + local field_name = vim.treesitter.get_node_text(node, buf) + if not vim.tbl_contains(parser_info.fields, field_name) then + add_lint_for_node(diagnostics, node, buf, 'Invalid field', lang) + end +end + +--- @private +--- @param node TSNode +--- @param buf integer +--- @param lang string +--- @param parser_info (table) +--- @param diagnostics Diagnostic[] +local function check_node(node, buf, lang, parser_info, diagnostics) + local node_type = vim.treesitter.get_node_text(node, buf) + local is_named = node_type:sub(1, 1) ~= '"' + + if not is_named then + node_type = node_type:gsub('"(.*)".*$', '%1'):gsub('\\(.)', '%1') + end + + local found = vim.tbl_contains(BUILT_IN_NODE_NAMES, node_type) + or vim.tbl_contains(parser_info.symbols, function(s) + return vim.deep_equal(s, { node_type, is_named }) + end, { predicate = true }) + + if not found then + add_lint_for_node(diagnostics, node, buf, 'Invalid node type', lang) + end +end + +--- @private +--- @param node TSNode +--- @param buf integer +--- @param is_predicate boolean +--- @return string +local function get_predicate_name(node, buf, is_predicate) + local name = vim.treesitter.get_node_text(node, buf) + if is_predicate then + if vim.startswith(name, 'not-') then + --- @type string + name = name:sub(string.len('not-') + 1) + end + return name .. '?' + end + return name .. '!' +end + +--- @private +--- @param predicate_node TSNode +--- @param predicate_type_node TSNode +--- @param buf integer +--- @param lang string? +--- @param diagnostics Diagnostic[] +local function check_predicate(predicate_node, predicate_type_node, buf, lang, diagnostics) + local type_string = vim.treesitter.get_node_text(predicate_type_node, buf) + + -- Quirk of the query grammar that directives are also predicates! + if type_string == '?' then + if + not vim.tbl_contains( + vim.treesitter.query.list_predicates(), + get_predicate_name(predicate_node, buf, true) + ) + then + add_lint_for_node(diagnostics, predicate_node, buf, 'Unknown predicate', lang) + end + elseif type_string == '!' then + if + not vim.tbl_contains( + vim.treesitter.query.list_directives(), + get_predicate_name(predicate_node, buf, false) + ) + then + add_lint_for_node(diagnostics, predicate_node, buf, 'Unknown directive', lang) + end + end +end + +--- @private +--- @param buf integer +--- @param match table +--- @param query Query +--- @param lang_context QueryLinterLanguageContext +--- @param diagnostics Diagnostic[] +local function lint_match(buf, match, query, lang_context, diagnostics) + local predicate --- @type TSNode + local predicate_type --- @type TSNode + local lang = lang_context.lang + local parser_info = lang_context.parser_info + + for id, node in pairs(match) do + local cap_id = query.captures[id] + + -- perform language-independent checks only for first lang + if lang_context.is_first_lang then + if cap_id == 'error' then + add_lint_for_node(diagnostics, node, buf, 'Syntax error') + elseif cap_id == 'predicate.name' then + predicate = node + elseif cap_id == 'predicate.type' then + predicate_type = node + end + end + + -- other checks rely on Neovim parser introspection + if lang and parser_info then + if cap_id == 'toplevel' then + check_toplevel(node, buf, lang, diagnostics) + elseif cap_id == 'field' then + check_field(node, buf, lang, parser_info, diagnostics) + elseif cap_id == 'node.named' or cap_id == 'node.anonymous' then + check_node(node, buf, lang, parser_info, diagnostics) + end + end + end + + if predicate and predicate_type then + check_predicate(predicate, predicate_type, buf, lang, diagnostics) + end +end + +--- @private +--- @param buf integer Buffer to lint +--- @param opts QueryLinterOpts|QueryLinterNormalizedOpts|nil Options for linting +function M.lint(buf, opts) + if buf == 0 then + buf = vim.api.nvim_get_current_buf() + end + + local diagnostics = {} + local query = vim.treesitter.query.parse('query', lint_query) + + opts = normalize_opts(buf, opts) + + -- perform at least one iteration even with no langs to perform language independent checks + for i = 1, math.max(1, #opts.langs) do + local lang = opts.langs[i] + + --- @type boolean, (table|nil) + local ok, parser_info = pcall(vim.treesitter.language.inspect, lang) + if not ok then + parser_info = nil + end + + local parser = vim.treesitter.get_parser(buf) + parser:parse() + parser:for_each_tree(function(tree, ltree) + if ltree:lang() == 'query' then + for _, match, _ in query:iter_matches(tree:root(), buf, 0, -1) do + local lang_context = { + lang = lang, + parser_info = parser_info, + is_first_lang = i == 1, + } + lint_match(buf, match, query, lang_context, diagnostics) + end + end + end) + end + + vim.diagnostic.set(namespace, buf, diagnostics) +end + +--- @private +--- @param buf integer +function M.clear(buf) + vim.diagnostic.reset(namespace, buf) +end + +return M -- cgit From 668f16bac779ac52d7bd9452e6001a7a6d1e9965 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sun, 30 Apr 2023 11:01:54 +0200 Subject: feat(treesitter): upstream query omnifunc from playground (#23394) and set by default in `ftplugin/query.lua` --- runtime/lua/vim/treesitter/_query_linter.lua | 60 ++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/treesitter/_query_linter.lua') diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua index 62f28d3097..ecdee5fc95 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -1,4 +1,6 @@ -local namespace = vim.api.nvim_create_namespace('vim.treesitter.query_linter') +local api = vim.api + +local namespace = api.nvim_create_namespace('vim.treesitter.query_linter') -- those node names exist for every language local BUILT_IN_NODE_NAMES = { '_', 'ERROR' } @@ -49,7 +51,7 @@ end --- @param buf integer --- @return string? local function guess_query_lang(buf) - local filename = vim.api.nvim_buf_get_name(buf) + local filename = api.nvim_buf_get_name(buf) if filename ~= '' then local ok, query_lang = pcall(vim.fn.fnamemodify, filename, ':p:h:t') if ok then @@ -256,7 +258,7 @@ end --- @param opts QueryLinterOpts|QueryLinterNormalizedOpts|nil Options for linting function M.lint(buf, opts) if buf == 0 then - buf = vim.api.nvim_get_current_buf() + buf = api.nvim_get_current_buf() end local diagnostics = {} @@ -299,4 +301,56 @@ function M.clear(buf) vim.diagnostic.reset(namespace, buf) end +--- @private +--- @param findstart integer +--- @param base string +function M.omnifunc(findstart, base) + if findstart == 1 then + local result = + api.nvim_get_current_line():sub(1, api.nvim_win_get_cursor(0)[2]):find('["#%-%w]*$') + return result - 1 + end + + local buf = api.nvim_get_current_buf() + local query_lang = guess_query_lang(buf) + + local ok, parser_info = pcall(vim.treesitter.language.inspect, query_lang) + if not ok then + return -2 + end + + local items = {} + for _, f in pairs(parser_info.fields) do + if f:find(base, 1, true) then + table.insert(items, f .. ':') + end + end + for _, p in pairs(vim.treesitter.query.list_predicates()) do + local text = '#' .. p + local found = text:find(base, 1, true) + if found and found <= 2 then -- with or without '#' + table.insert(items, text) + end + text = '#not-' .. p + found = text:find(base, 1, true) + if found and found <= 2 then -- with or without '#' + table.insert(items, text) + end + end + for _, p in pairs(vim.treesitter.query.list_directives()) do + local text = '#' .. p + local found = text:find(base, 1, true) + if found and found <= 2 then -- with or without '#' + table.insert(items, text) + end + end + for _, s in pairs(parser_info.symbols) do + local text = s[2] and s[1] or '"' .. s[1]:gsub([[\]], [[\\]]) .. '"' + if text:find(base, 1, true) then + table.insert(items, text) + end + end + return { words = items, refresh = 'always' } +end + return M -- cgit From ceb37ff1d83c6dc341dc43d3dc8c81f657bc9484 Mon Sep 17 00:00:00 2001 From: Stephan Seitz Date: Sun, 11 Jun 2023 23:46:22 +0200 Subject: refactor(treesitter): use npcall in _query_linter.lua #23985 --- runtime/lua/vim/treesitter/_query_linter.lua | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) (limited to 'runtime/lua/vim/treesitter/_query_linter.lua') diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua index ecdee5fc95..3dd0177a81 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -53,10 +53,7 @@ end local function guess_query_lang(buf) local filename = api.nvim_buf_get_name(buf) if filename ~= '' then - local ok, query_lang = pcall(vim.fn.fnamemodify, filename, ':p:h:t') - if ok then - return query_lang - end + return vim.F.npcall(vim.fn.fnamemodify, filename, ':p:h:t') end end @@ -270,11 +267,8 @@ function M.lint(buf, opts) for i = 1, math.max(1, #opts.langs) do local lang = opts.langs[i] - --- @type boolean, (table|nil) - local ok, parser_info = pcall(vim.treesitter.language.inspect, lang) - if not ok then - parser_info = nil - end + --- @type (table|nil) + local parser_info = vim.F.npcall(vim.treesitter.language.inspect, lang) local parser = vim.treesitter.get_parser(buf) parser:parse() -- cgit From 6debb1852355e0112ce75a5b4aed714ba1469ddb Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Fri, 15 Sep 2023 12:45:40 -0700 Subject: refactor(treesitter): remove duplicated diagnostic code (#24976) * refactor(treesitter): remove duplicated diagnostic code * fixup!: fix type errors * fixup!: add type namespace --- runtime/lua/vim/treesitter/_query_linter.lua | 177 ++++++++------------------- 1 file changed, 48 insertions(+), 129 deletions(-) (limited to 'runtime/lua/vim/treesitter/_query_linter.lua') diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua index 3dd0177a81..abf0bf345d 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -1,8 +1,6 @@ local api = vim.api local namespace = api.nvim_create_namespace('vim.treesitter.query_linter') --- those node names exist for every language -local BUILT_IN_NODE_NAMES = { '_', 'ERROR' } local M = {} @@ -10,11 +8,13 @@ local M = {} --- @field langs string[] --- @field clear boolean +--- @alias vim.treesitter.ParseError {msg: string, range: Range4} + --- @private --- Caches parse results for queries for each language. --- Entries of parse_cache[lang][query_text] will either be true for successful parse or contain the ---- error message of the parse ---- @type table> +--- message and range of the parse error. +--- @type table> local parse_cache = {} --- Contains language dependent context for the query linter @@ -26,20 +26,16 @@ local parse_cache = {} --- @private --- Adds a diagnostic for node in the query buffer --- @param diagnostics Diagnostic[] ---- @param node TSNode ---- @param buf integer +--- @param range Range4 --- @param lint string --- @param lang string? -local function add_lint_for_node(diagnostics, node, buf, lint, lang) - local node_text = vim.treesitter.get_node_text(node, buf):gsub('\n', ' ') - --- @type string - local message = lint .. ': ' .. node_text - local error_range = { node:range() } +local function add_lint_for_node(diagnostics, range, lint, lang) + local message = lint:gsub('\n', ' ') diagnostics[#diagnostics + 1] = { - lnum = error_range[1], - end_lnum = error_range[3], - col = error_range[2], - end_col = error_range[4], + lnum = range[1], + end_lnum = range[3], + col = range[2], + end_col = range[4], severity = vim.diagnostic.ERROR, message = message, source = lang, @@ -91,6 +87,31 @@ local lint_query = [[;; query (ERROR) @error ]] +--- @private +--- @param err string +--- @param node TSNode +--- @return vim.treesitter.ParseError +local function get_error_entry(err, node) + local start_line, start_col = node:range() + local line_offset, col_offset, msg = err:gmatch('.-:%d+: Query error at (%d+):(%d+)%. ([^:]+)')() ---@type string, string, string + start_line, start_col = + start_line + tonumber(line_offset) - 1, start_col + tonumber(col_offset) - 1 + local end_line, end_col = start_line, start_col + if msg:match('^Invalid syntax') or msg:match('^Impossible') then + -- Use the length of the underlined node + local underlined = vim.split(err, '\n')[2] + end_col = end_col + #underlined + elseif msg:match('^Invalid') then + -- Use the length of the problematic type/capture/field + end_col = end_col + #msg:match('"([^"]+)"') + end + + return { + msg = msg, + range = { start_line, start_col, end_line, end_col }, + } +end + --- @private --- @param node TSNode --- @param buf integer @@ -106,104 +127,19 @@ local function check_toplevel(node, buf, lang, diagnostics) local lang_cache = parse_cache[lang] if lang_cache[query_text] == nil then - local ok, err = pcall(vim.treesitter.query.parse, lang, query_text) + local cache_val, err = pcall(vim.treesitter.query.parse, lang, query_text) ---@type boolean|vim.treesitter.ParseError, string|Query - if not ok and type(err) == 'string' then - err = err:match('.-:%d+: (.+)') + if not cache_val and type(err) == 'string' then + cache_val = get_error_entry(err, node) end - lang_cache[query_text] = ok or err + lang_cache[query_text] = cache_val end local cache_entry = lang_cache[query_text] - if type(cache_entry) == 'string' then - add_lint_for_node(diagnostics, node, buf, cache_entry, lang) - end -end - ---- @private ---- @param node TSNode ---- @param buf integer ---- @param lang string ---- @param parser_info table ---- @param diagnostics Diagnostic[] -local function check_field(node, buf, lang, parser_info, diagnostics) - local field_name = vim.treesitter.get_node_text(node, buf) - if not vim.tbl_contains(parser_info.fields, field_name) then - add_lint_for_node(diagnostics, node, buf, 'Invalid field', lang) - end -end - ---- @private ---- @param node TSNode ---- @param buf integer ---- @param lang string ---- @param parser_info (table) ---- @param diagnostics Diagnostic[] -local function check_node(node, buf, lang, parser_info, diagnostics) - local node_type = vim.treesitter.get_node_text(node, buf) - local is_named = node_type:sub(1, 1) ~= '"' - - if not is_named then - node_type = node_type:gsub('"(.*)".*$', '%1'):gsub('\\(.)', '%1') - end - - local found = vim.tbl_contains(BUILT_IN_NODE_NAMES, node_type) - or vim.tbl_contains(parser_info.symbols, function(s) - return vim.deep_equal(s, { node_type, is_named }) - end, { predicate = true }) - - if not found then - add_lint_for_node(diagnostics, node, buf, 'Invalid node type', lang) - end -end - ---- @private ---- @param node TSNode ---- @param buf integer ---- @param is_predicate boolean ---- @return string -local function get_predicate_name(node, buf, is_predicate) - local name = vim.treesitter.get_node_text(node, buf) - if is_predicate then - if vim.startswith(name, 'not-') then - --- @type string - name = name:sub(string.len('not-') + 1) - end - return name .. '?' - end - return name .. '!' -end - ---- @private ---- @param predicate_node TSNode ---- @param predicate_type_node TSNode ---- @param buf integer ---- @param lang string? ---- @param diagnostics Diagnostic[] -local function check_predicate(predicate_node, predicate_type_node, buf, lang, diagnostics) - local type_string = vim.treesitter.get_node_text(predicate_type_node, buf) - - -- Quirk of the query grammar that directives are also predicates! - if type_string == '?' then - if - not vim.tbl_contains( - vim.treesitter.query.list_predicates(), - get_predicate_name(predicate_node, buf, true) - ) - then - add_lint_for_node(diagnostics, predicate_node, buf, 'Unknown predicate', lang) - end - elseif type_string == '!' then - if - not vim.tbl_contains( - vim.treesitter.query.list_directives(), - get_predicate_name(predicate_node, buf, false) - ) - then - add_lint_for_node(diagnostics, predicate_node, buf, 'Unknown directive', lang) - end + if type(cache_entry) ~= 'boolean' then + add_lint_for_node(diagnostics, cache_entry.range, cache_entry.msg, lang) end end @@ -214,8 +150,6 @@ end --- @param lang_context QueryLinterLanguageContext --- @param diagnostics Diagnostic[] local function lint_match(buf, match, query, lang_context, diagnostics) - local predicate --- @type TSNode - local predicate_type --- @type TSNode local lang = lang_context.lang local parser_info = lang_context.parser_info @@ -223,31 +157,16 @@ local function lint_match(buf, match, query, lang_context, diagnostics) local cap_id = query.captures[id] -- perform language-independent checks only for first lang - if lang_context.is_first_lang then - if cap_id == 'error' then - add_lint_for_node(diagnostics, node, buf, 'Syntax error') - elseif cap_id == 'predicate.name' then - predicate = node - elseif cap_id == 'predicate.type' then - predicate_type = node - end + if lang_context.is_first_lang and cap_id == 'error' then + local node_text = vim.treesitter.get_node_text(node, buf):gsub('\n', ' ') + add_lint_for_node(diagnostics, { node:range() }, 'Syntax error: ' .. node_text) end -- other checks rely on Neovim parser introspection - if lang and parser_info then - if cap_id == 'toplevel' then - check_toplevel(node, buf, lang, diagnostics) - elseif cap_id == 'field' then - check_field(node, buf, lang, parser_info, diagnostics) - elseif cap_id == 'node.named' or cap_id == 'node.anonymous' then - check_node(node, buf, lang, parser_info, diagnostics) - end + if lang and parser_info and cap_id == 'toplevel' then + check_toplevel(node, buf, lang, diagnostics) end end - - if predicate and predicate_type then - check_predicate(predicate, predicate_type, buf, lang, diagnostics) - end end --- @private @@ -339,7 +258,7 @@ function M.omnifunc(findstart, base) end end for _, s in pairs(parser_info.symbols) do - local text = s[2] and s[1] or '"' .. s[1]:gsub([[\]], [[\\]]) .. '"' + local text = s[2] and s[1] or '"' .. s[1]:gsub([[\]], [[\\]]) .. '"' ---@type string if text:find(base, 1, true) then table.insert(items, text) end -- cgit From 877d04d0fb83b5fc602dbab22b58f26a793ec236 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sat, 16 Sep 2023 23:10:30 +0100 Subject: feat(lua): add vim.func._memoize Memoizes a function, using a custom function to hash the arguments. Private for now until: - There are other places in the codebase that could benefit from this (e.g. LSP), but might require other changes to accommodate. - Invalidation of the cache needs to be controllable. Using weak tables is an acceptable invalidation policy, but it shouldn't be the only one. - I don't think the story around `hash_fn` is completely thought out. We may be able to have a good default hash_fn by hashing each argument, so basically a better 'concat'. --- runtime/lua/vim/treesitter/_query_linter.lua | 54 +++++++++------------------- 1 file changed, 17 insertions(+), 37 deletions(-) (limited to 'runtime/lua/vim/treesitter/_query_linter.lua') diff --git a/runtime/lua/vim/treesitter/_query_linter.lua b/runtime/lua/vim/treesitter/_query_linter.lua index abf0bf345d..87d74789a3 100644 --- a/runtime/lua/vim/treesitter/_query_linter.lua +++ b/runtime/lua/vim/treesitter/_query_linter.lua @@ -10,20 +10,12 @@ local M = {} --- @alias vim.treesitter.ParseError {msg: string, range: Range4} ---- @private ---- Caches parse results for queries for each language. ---- Entries of parse_cache[lang][query_text] will either be true for successful parse or contain the ---- message and range of the parse error. ---- @type table> -local parse_cache = {} - --- Contains language dependent context for the query linter --- @class QueryLinterLanguageContext --- @field lang string? Current `lang` of the targeted parser --- @field parser_info table? Parser info returned by vim.treesitter.language.inspect --- @field is_first_lang boolean Whether this is the first language of a linter run checking queries for multiple `langs` ---- @private --- Adds a diagnostic for node in the query buffer --- @param diagnostics Diagnostic[] --- @param range Range4 @@ -42,7 +34,6 @@ local function add_lint_for_node(diagnostics, range, lint, lang) } end ---- @private --- Determines the target language of a query file by its path: /.scm --- @param buf integer --- @return string? @@ -53,7 +44,6 @@ local function guess_query_lang(buf) end end ---- @private --- @param buf integer --- @param opts QueryLinterOpts|QueryLinterNormalizedOpts|nil --- @return QueryLinterNormalizedOpts @@ -87,7 +77,6 @@ local lint_query = [[;; query (ERROR) @error ]] ---- @private --- @param err string --- @param node TSNode --- @return vim.treesitter.ParseError @@ -112,38 +101,26 @@ local function get_error_entry(err, node) } end ---- @private --- @param node TSNode --- @param buf integer --- @param lang string ---- @param diagnostics Diagnostic[] -local function check_toplevel(node, buf, lang, diagnostics) - local query_text = vim.treesitter.get_node_text(node, buf) - - if not parse_cache[lang] then - parse_cache[lang] = {} - end - - local lang_cache = parse_cache[lang] - - if lang_cache[query_text] == nil then - local cache_val, err = pcall(vim.treesitter.query.parse, lang, query_text) ---@type boolean|vim.treesitter.ParseError, string|Query - - if not cache_val and type(err) == 'string' then - cache_val = get_error_entry(err, node) - end - - lang_cache[query_text] = cache_val - end +local function hash_parse(node, buf, lang) + return tostring(node:id()) .. tostring(buf) .. tostring(vim.b[buf].changedtick) .. lang +end - local cache_entry = lang_cache[query_text] +--- @param node TSNode +--- @param buf integer +--- @param lang string +--- @return vim.treesitter.ParseError? +local parse = vim.func._memoize(hash_parse, function(node, buf, lang) + local query_text = vim.treesitter.get_node_text(node, buf) + local ok, err = pcall(vim.treesitter.query.parse, lang, query_text) ---@type boolean|vim.treesitter.ParseError, string|Query - if type(cache_entry) ~= 'boolean' then - add_lint_for_node(diagnostics, cache_entry.range, cache_entry.msg, lang) + if not ok and type(err) == 'string' then + return get_error_entry(err, node) end -end +end) ---- @private --- @param buf integer --- @param match table --- @param query Query @@ -164,7 +141,10 @@ local function lint_match(buf, match, query, lang_context, diagnostics) -- other checks rely on Neovim parser introspection if lang and parser_info and cap_id == 'toplevel' then - check_toplevel(node, buf, lang, diagnostics) + local err = parse(node, buf, lang) + if err then + add_lint_for_node(diagnostics, err.range, err.msg, lang) + end end end end -- cgit