From 845d5b8b64190e0e09a6a6dd97bdbc0e6f96eb02 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Wed, 19 Jul 2023 05:02:49 -0400 Subject: [PATCH] feat(treesitter): improve query error message --- runtime/doc/news.txt | 1 + src/nvim/lua/treesitter.c | 80 +++++++++++++++++++--- test/functional/lua/overrides_spec.lua | 6 +- test/functional/treesitter/parser_spec.lua | 40 +++++++++++ test/helpers.lua | 8 ++- 5 files changed, 122 insertions(+), 13 deletions(-) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index a233b66d1f..7607d218c3 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -135,6 +135,7 @@ The following new APIs and features were added. or the parent LanguageTree's language, respectively. • Added `vim.treesitter.preview_query()`, for live editing of treesitter queries. + • Improved error messages for query parsing. • |vim.ui.open()| opens URIs using the system default handler (macOS `open`, Windows `explorer`, Linux `xdg-open`, etc.) diff --git a/src/nvim/lua/treesitter.c b/src/nvim/lua/treesitter.c index 49e920f748..48057b0c65 100644 --- a/src/nvim/lua/treesitter.c +++ b/src/nvim/lua/treesitter.c @@ -1543,8 +1543,9 @@ int tslua_parse_query(lua_State *L) TSQuery *query = ts_query_new(lang, src, (uint32_t)len, &error_offset, &error_type); if (!query) { - return luaL_error(L, "query: %s at position %d for language %s", - query_err_string(error_type), (int)error_offset, lang_name); + char err_msg[IOSIZE]; + query_err_string(src, (int)error_offset, error_type, err_msg, sizeof(err_msg)); + return luaL_error(L, "%s", err_msg); } TSQuery **ud = lua_newuserdata(L, sizeof(TSQuery *)); // [udata] @@ -1554,24 +1555,85 @@ int tslua_parse_query(lua_State *L) return 1; } -static const char *query_err_string(TSQueryError err) +static const char *query_err_to_string(TSQueryError error_type) { - switch (err) { + switch (error_type) { case TSQueryErrorSyntax: - return "invalid syntax"; + return "Invalid syntax:\n"; case TSQueryErrorNodeType: - return "invalid node type"; + return "Invalid node type "; case TSQueryErrorField: - return "invalid field"; + return "Invalid field name "; case TSQueryErrorCapture: - return "invalid capture"; + return "Invalid capture name "; case TSQueryErrorStructure: - return "invalid structure"; + return "Impossible pattern:\n"; default: return "error"; } } +static void query_err_string(const char *src, int error_offset, TSQueryError error_type, char *err, + size_t errlen) +{ + int line_start = 0; + int row = 0; + const char *error_line = NULL; + int error_line_len = 0; + + const char *end_str; + const char *src_tmp = src; + while ((end_str = strchr(src_tmp, '\n')) != NULL) { + int line_length = (int)(end_str - src_tmp) + 1; + int line_end = line_start + line_length; + if (line_end > error_offset) { + error_line = src_tmp; + error_line_len = line_length; + break; + } + line_start = line_end; + row++; + src_tmp += line_length; + } + + // Additional check for the last line + if (line_start <= error_offset) { + error_line = src_tmp; + error_line_len = (int)strlen(src_tmp); + } + + int column = error_offset - line_start; + + const char *type_msg = query_err_to_string(error_type); + snprintf(err, errlen, "Query error at %d:%d. %s", row + 1, column + 1, type_msg); + size_t offset = strlen(err); + errlen = errlen - offset; + err = err + offset; + + // Error types that report names + if (error_type == TSQueryErrorNodeType + || error_type == TSQueryErrorField + || error_type == TSQueryErrorCapture) { + const char *suffix = src + error_offset; + int suffix_len = 0; + char c = suffix[suffix_len]; + while (isalnum(c) || c == '_' || c == '-' || c == '.') { + c = suffix[++suffix_len]; + } + snprintf(err, errlen, "\"%.*s\":\n", suffix_len, suffix); + offset = strlen(err); + errlen = errlen - offset; + err = err + offset; + } + + if (!error_line) { + snprintf(err, errlen, "Unexpected EOF\n"); + return; + } + + snprintf(err, errlen, "%.*s\n%*s^\n", error_line_len, error_line, column, ""); +} + static TSQuery *query_check(lua_State *L, int index) { TSQuery **ud = luaL_checkudata(L, index, TS_META_QUERY); diff --git a/test/functional/lua/overrides_spec.lua b/test/functional/lua/overrides_spec.lua index 1777dd078d..c08f3d06a9 100644 --- a/test/functional/lua/overrides_spec.lua +++ b/test/functional/lua/overrides_spec.lua @@ -54,7 +54,7 @@ describe('print', function() -- TODO(bfredl): these look weird, print() should not use "E5114:" style errors.. eq('Vim(lua):E5108: Error executing lua E5114: Error while converting print argument #2: [NULL]', pcall_err(command, 'lua print("foo", v_nilerr, "bar")')) - eq('Vim(lua):E5108: Error executing lua E5114: Error while converting print argument #2: Xtest-functional-lua-overrides-luafile:0: abc', + eq('Vim(lua):E5108: Error executing lua E5114: Error while converting print argument #2: Xtest-functional-lua-overrides-luafile:2: abc', pcall_err(command, 'lua print("foo", v_abcerr, "bar")')) eq('Vim(lua):E5108: Error executing lua E5114: Error while converting print argument #2: ', pcall_err(command, 'lua print("foo", v_tblout, "bar")')) @@ -84,9 +84,9 @@ describe('print', function() end ]]) eq('', exec_capture('luafile ' .. fname)) - eq('Vim(lua):E5108: Error executing lua Xtest-functional-lua-overrides-luafile:0: my mistake', + eq('Vim(lua):E5108: Error executing lua Xtest-functional-lua-overrides-luafile:1: my mistake', pcall_err(command, 'lua string_error()')) - eq('Vim(lua):E5108: Error executing lua Xtest-functional-lua-overrides-luafile:0: 1234', + eq('Vim(lua):E5108: Error executing lua Xtest-functional-lua-overrides-luafile:2: 1234', pcall_err(command, 'lua number_error()')) eq('Vim(lua):E5108: Error executing lua [NULL]', pcall_err(command, 'lua nil_error()')) diff --git a/test/functional/treesitter/parser_spec.lua b/test/functional/treesitter/parser_spec.lua index 56af0c8738..37dde37a64 100644 --- a/test/functional/treesitter/parser_spec.lua +++ b/test/functional/treesitter/parser_spec.lua @@ -1055,4 +1055,44 @@ int x = INT_MAX; ]]) 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 1:30. Invalid capture name "ok.capture":\n'.. + '((identifier) @id (#eq? @id @ok.capture))\n'.. + ' ^', + '((identifier) @id (#eq? @id @ok.capture))') + end) end) diff --git a/test/helpers.lua b/test/helpers.lua index 51114611ab..02192e4924 100644 --- a/test/helpers.lua +++ b/test/helpers.lua @@ -188,10 +188,16 @@ function module.pcall(fn, ...) local errmsg = tostring(rv):gsub('([%s<])vim[/\\]([^%s:/\\]+):%d+', '%1\xffvim\xff%2:0') :gsub('[^%s<]-[/\\]([^%s:/\\]+):%d+', '.../%1:0') :gsub('\xffvim\xff', 'vim/') + -- Scrub numbers in paths/stacktraces: -- shared.lua:0: in function 'gsplit' -- shared.lua:0: in function ' - errmsg = errmsg:gsub('([^%s]):%d+', '%1:0') + errmsg = errmsg:gsub('([^%s].lua):%d+', '%1:0') + -- [string ""]:0: + -- [string ":lua"]:0: + -- [string ":luado"]:0: + errmsg = errmsg:gsub('(%[string "[^"]+"%]):%d+', '%1:0') + -- Scrub tab chars: errmsg = errmsg:gsub('\t', ' ') -- In Lua 5.1, we sometimes get a "(tail call): ?" on the last line.