mirror of
https://github.com/neovim/neovim.git
synced 2025-02-25 18:55:25 -06:00
vim-patch:9.1.0984: exception handling can be improved
Problem: exception handling can be improved
Solution: add v:stacktrace and getstacktrace()
closes: vim/vim#16360
663d18d610
Co-authored-by: ichizok <gclient.gaap@gmail.com>
Co-authored-by: Naruhiko Nishino <naru123456789@gmail.com>
This commit is contained in:
parent
06ff5480ce
commit
d5308637bf
15
runtime/doc/builtin.txt
generated
15
runtime/doc/builtin.txt
generated
@ -4182,6 +4182,21 @@ getscriptinfo([{opts}]) *getscriptinfo()*
|
||||
Return: ~
|
||||
(`vim.fn.getscriptinfo.ret[]`)
|
||||
|
||||
getstacktrace() *getstacktrace()*
|
||||
Returns the current stack trace of Vim scripts.
|
||||
Stack trace is a |List|, of which each item is a |Dictionary|
|
||||
with the following items:
|
||||
funcref The funcref if the stack is at the function,
|
||||
otherwise this item is not exist.
|
||||
event The string of the event description if the
|
||||
stack is at autocmd event, otherwise this item
|
||||
is not exist.
|
||||
lnum The line number of the script on the stack.
|
||||
filepath The file path of the script on the stack.
|
||||
|
||||
Return: ~
|
||||
(`table[]`)
|
||||
|
||||
gettabinfo([{tabnr}]) *gettabinfo()*
|
||||
If {tabnr} is not specified, then information about all the
|
||||
tab pages is returned as a |List|. Each List item is a
|
||||
|
@ -2848,7 +2848,8 @@ in the variable |v:exception|: >
|
||||
: echo "Number thrown. Value is" v:exception
|
||||
|
||||
You may also be interested where an exception was thrown. This is stored in
|
||||
|v:throwpoint|. Note that "v:exception" and "v:throwpoint" are valid for the
|
||||
|v:throwpoint|. And you can obtain the stack trace from |v:stacktrace|.
|
||||
Note that "v:exception", "v:stacktrace" and "v:throwpoint" are valid for the
|
||||
exception most recently caught as long it is not finished.
|
||||
Example: >
|
||||
|
||||
|
@ -1103,7 +1103,8 @@ Various: *various-functions*
|
||||
did_filetype() check if a FileType autocommand was used
|
||||
eventhandler() check if invoked by an event handler
|
||||
getpid() get process ID of Vim
|
||||
getscriptinfo() get list of sourced vim scripts
|
||||
getscriptinfo() get list of sourced Vim scripts
|
||||
getstacktrace() get current stack trace of Vim scripts
|
||||
|
||||
libcall() call a function in an external library
|
||||
libcallnr() idem, returning a number
|
||||
|
@ -6,7 +6,8 @@
|
||||
|
||||
Predefined variables *vvars*
|
||||
|
||||
Some variables can be set by the user, but the type cannot be changed.
|
||||
Most variables are read-only, when a variable can be set by the user, it will
|
||||
be mentioned at the variable description below. The type cannot be changed.
|
||||
|
||||
Type |gO| to see the table of contents.
|
||||
|
||||
@ -195,7 +196,8 @@ v:event
|
||||
*v:exception* *exception-variable*
|
||||
v:exception
|
||||
The value of the exception most recently caught and not
|
||||
finished. See also |v:throwpoint| and |throw-variables|.
|
||||
finished. See also |v:stacktrace|, |v:throwpoint|, and
|
||||
|throw-variables|.
|
||||
Example: >vim
|
||||
try
|
||||
throw "oops"
|
||||
@ -586,6 +588,13 @@ v:shell_error
|
||||
endif
|
||||
<
|
||||
|
||||
*v:stacktrace* *stacktrace-variable*
|
||||
v:stacktrace
|
||||
The stack trace of the exception most recently caught and
|
||||
not finished. Refer to |getstacktrace()| for the structure of
|
||||
stack trace. See also |v:exception|, |v:throwpoint|, and
|
||||
|throw-variables|.
|
||||
|
||||
*v:statusmsg* *statusmsg-variable*
|
||||
v:statusmsg
|
||||
Last given status message.
|
||||
@ -679,7 +688,7 @@ v:this_session
|
||||
v:throwpoint
|
||||
The point where the exception most recently caught and not
|
||||
finished was thrown. Not set when commands are typed. See
|
||||
also |v:exception| and |throw-variables|.
|
||||
also |v:exception|, |v:stacktrace|, and |throw-variables|.
|
||||
Example: >vim
|
||||
try
|
||||
throw "oops"
|
||||
|
14
runtime/lua/vim/_meta/vimfn.lua
generated
14
runtime/lua/vim/_meta/vimfn.lua
generated
@ -3770,6 +3770,20 @@ function vim.fn.getregtype(regname) end
|
||||
--- @return vim.fn.getscriptinfo.ret[]
|
||||
function vim.fn.getscriptinfo(opts) end
|
||||
|
||||
--- Returns the current stack trace of Vim scripts.
|
||||
--- Stack trace is a |List|, of which each item is a |Dictionary|
|
||||
--- with the following items:
|
||||
--- funcref The funcref if the stack is at the function,
|
||||
--- otherwise this item is not exist.
|
||||
--- event The string of the event description if the
|
||||
--- stack is at autocmd event, otherwise this item
|
||||
--- is not exist.
|
||||
--- lnum The line number of the script on the stack.
|
||||
--- filepath The file path of the script on the stack.
|
||||
---
|
||||
--- @return table[]
|
||||
function vim.fn.getstacktrace() end
|
||||
|
||||
--- If {tabnr} is not specified, then information about all the
|
||||
--- tab pages is returned as a |List|. Each List item is a
|
||||
--- |Dictionary|. Otherwise, {tabnr} specifies the tab page
|
||||
|
12
runtime/lua/vim/_meta/vvars.lua
generated
12
runtime/lua/vim/_meta/vvars.lua
generated
@ -203,7 +203,8 @@ vim.v.errors = ...
|
||||
vim.v.event = ...
|
||||
|
||||
--- The value of the exception most recently caught and not
|
||||
--- finished. See also `v:throwpoint` and `throw-variables`.
|
||||
--- finished. See also `v:stacktrace`, `v:throwpoint`, and
|
||||
--- `throw-variables`.
|
||||
--- Example:
|
||||
---
|
||||
--- ```vim
|
||||
@ -616,6 +617,13 @@ vim.v.servername = ...
|
||||
--- @type integer
|
||||
vim.v.shell_error = ...
|
||||
|
||||
--- The stack trace of the exception most recently caught and
|
||||
--- not finished. Refer to `getstacktrace()` for the structure of
|
||||
--- stack trace. See also `v:exception`, `v:throwpoint`, and
|
||||
--- `throw-variables`.
|
||||
--- @type table[]
|
||||
vim.v.stacktrace = ...
|
||||
|
||||
--- Last given status message.
|
||||
--- Modifiable (can be set).
|
||||
--- @type string
|
||||
@ -718,7 +726,7 @@ vim.v.this_session = ...
|
||||
|
||||
--- The point where the exception most recently caught and not
|
||||
--- finished was thrown. Not set when commands are typed. See
|
||||
--- also `v:exception` and `throw-variables`.
|
||||
--- also `v:exception`, `v:stacktrace`, and `throw-variables`.
|
||||
--- Example:
|
||||
---
|
||||
--- ```vim
|
||||
|
@ -270,6 +270,7 @@ static struct vimvar {
|
||||
VV(VV_COLLATE, "collate", VAR_STRING, VV_RO),
|
||||
VV(VV_EXITING, "exiting", VAR_NUMBER, VV_RO),
|
||||
VV(VV_MAXCOL, "maxcol", VAR_NUMBER, VV_RO),
|
||||
VV(VV_STACKTRACE, "stacktrace", VAR_LIST, VV_RO),
|
||||
// Neovim
|
||||
VV(VV_STDERR, "stderr", VAR_NUMBER, VV_RO),
|
||||
VV(VV_MSGPACK_TYPES, "msgpack_types", VAR_DICT, VV_RO),
|
||||
|
@ -167,6 +167,7 @@ typedef enum {
|
||||
VV_COLLATE,
|
||||
VV_EXITING,
|
||||
VV_MAXCOL,
|
||||
VV_STACKTRACE,
|
||||
// Nvim
|
||||
VV_STDERR,
|
||||
VV_MSGPACK_TYPES,
|
||||
|
@ -4670,6 +4670,25 @@ M.funcs = {
|
||||
returns = 'vim.fn.getscriptinfo.ret[]',
|
||||
signature = 'getscriptinfo([{opts}])',
|
||||
},
|
||||
getstacktrace = {
|
||||
args = 0,
|
||||
desc = [=[
|
||||
Returns the current stack trace of Vim scripts.
|
||||
Stack trace is a |List|, of which each item is a |Dictionary|
|
||||
with the following items:
|
||||
funcref The funcref if the stack is at the function,
|
||||
otherwise this item is not exist.
|
||||
event The string of the event description if the
|
||||
stack is at autocmd event, otherwise this item
|
||||
is not exist.
|
||||
lnum The line number of the script on the stack.
|
||||
filepath The file path of the script on the stack.
|
||||
]=],
|
||||
name = 'getstacktrace',
|
||||
params = {},
|
||||
returns = 'table[]',
|
||||
signature = 'getstacktrace()',
|
||||
},
|
||||
gettabinfo = {
|
||||
args = { 0, 1 },
|
||||
base = 1,
|
||||
|
@ -2633,6 +2633,30 @@ int tv_dict_add_allocated_str(dict_T *const d, const char *const key, const size
|
||||
return OK;
|
||||
}
|
||||
|
||||
/// Add a function entry to dictionary.
|
||||
///
|
||||
/// @param[out] d Dictionary to add entry to.
|
||||
/// @param[in] key Key to add.
|
||||
/// @param[in] key_len Key length.
|
||||
/// @param[in] fp Function to add.
|
||||
///
|
||||
/// @return OK in case of success, FAIL when key already exists.
|
||||
int tv_dict_add_func(dict_T *const d, const char *const key, const size_t key_len,
|
||||
ufunc_T *const fp)
|
||||
FUNC_ATTR_NONNULL_ARG(1, 2, 4)
|
||||
{
|
||||
dictitem_T *const item = tv_dict_item_alloc_len(key, key_len);
|
||||
|
||||
item->di_tv.v_type = VAR_FUNC;
|
||||
item->di_tv.vval.v_string = xstrdup(fp->uf_name);
|
||||
if (tv_dict_add(d, item) == FAIL) {
|
||||
tv_dict_item_free(item);
|
||||
return FAIL;
|
||||
}
|
||||
func_ref(item->di_tv.vval.v_string);
|
||||
return OK;
|
||||
}
|
||||
|
||||
//{{{2 Operations on the whole dict
|
||||
|
||||
/// Clear all the keys of a Dictionary. "d" remains a valid empty Dictionary.
|
||||
|
@ -479,6 +479,9 @@ static int throw_exception(void *value, except_type_T type, char *cmdname)
|
||||
excp->throw_lnum = SOURCING_LNUM;
|
||||
}
|
||||
|
||||
excp->stacktrace = stacktrace_create();
|
||||
tv_list_ref(excp->stacktrace);
|
||||
|
||||
if (p_verbose >= 13 || debug_break_level > 0) {
|
||||
int save_msg_silent = msg_silent;
|
||||
|
||||
@ -563,6 +566,7 @@ static void discard_exception(except_T *excp, bool was_finished)
|
||||
free_msglist(excp->messages);
|
||||
}
|
||||
xfree(excp->throw_name);
|
||||
tv_list_unref(excp->stacktrace);
|
||||
xfree(excp);
|
||||
}
|
||||
|
||||
@ -584,6 +588,7 @@ static void catch_exception(except_T *excp)
|
||||
excp->caught = caught_stack;
|
||||
caught_stack = excp;
|
||||
set_vim_var_string(VV_EXCEPTION, excp->value, -1);
|
||||
set_vim_var_list(VV_STACKTRACE, excp->stacktrace);
|
||||
if (*excp->throw_name != NUL) {
|
||||
if (excp->throw_lnum != 0) {
|
||||
vim_snprintf(IObuff, IOSIZE, _("%s, line %" PRId64),
|
||||
@ -633,6 +638,7 @@ static void finish_exception(except_T *excp)
|
||||
caught_stack = caught_stack->caught;
|
||||
if (caught_stack != NULL) {
|
||||
set_vim_var_string(VV_EXCEPTION, caught_stack->value, -1);
|
||||
set_vim_var_list(VV_STACKTRACE, caught_stack->stacktrace);
|
||||
if (*caught_stack->throw_name != NUL) {
|
||||
if (caught_stack->throw_lnum != 0) {
|
||||
vim_snprintf(IObuff, IOSIZE,
|
||||
@ -651,6 +657,7 @@ static void finish_exception(except_T *excp)
|
||||
} else {
|
||||
set_vim_var_string(VV_EXCEPTION, NULL, -1);
|
||||
set_vim_var_string(VV_THROWPOINT, NULL, -1);
|
||||
set_vim_var_list(VV_STACKTRACE, NULL);
|
||||
}
|
||||
|
||||
// Discard the exception, but use the finish message for 'verbose'.
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "nvim/eval/typval_defs.h"
|
||||
#include "nvim/pos_defs.h"
|
||||
|
||||
/// A list used for saving values of "emsg_silent". Used by ex_try() to save the
|
||||
@ -107,6 +108,7 @@ struct vim_exception {
|
||||
msglist_T *messages; ///< message(s) causing error exception
|
||||
char *throw_name; ///< name of the throw point
|
||||
linenr_T throw_lnum; ///< line number of the throw point
|
||||
list_T *stacktrace; ///< stacktrace
|
||||
except_T *caught; ///< next exception on the caught stack
|
||||
};
|
||||
|
||||
|
@ -228,6 +228,72 @@ char *estack_sfile(estack_arg_T which)
|
||||
return (char *)ga.ga_data;
|
||||
}
|
||||
|
||||
static void stacktrace_push_item(list_T *const l, ufunc_T *const fp, const char *const event,
|
||||
const linenr_T lnum, char *const filepath,
|
||||
const bool filepath_alloced)
|
||||
{
|
||||
dict_T *const d = tv_dict_alloc_lock(VAR_FIXED);
|
||||
typval_T tv = {
|
||||
.v_type = VAR_DICT,
|
||||
.v_lock = VAR_LOCKED,
|
||||
.vval.v_dict = d,
|
||||
};
|
||||
|
||||
if (fp != NULL) {
|
||||
tv_dict_add_func(d, S_LEN("funcref"), fp);
|
||||
}
|
||||
if (event != NULL) {
|
||||
tv_dict_add_str(d, S_LEN("event"), event);
|
||||
}
|
||||
tv_dict_add_nr(d, S_LEN("lnum"), lnum);
|
||||
if (filepath_alloced) {
|
||||
tv_dict_add_allocated_str(d, S_LEN("filepath"), filepath);
|
||||
} else {
|
||||
tv_dict_add_str(d, S_LEN("filepath"), filepath);
|
||||
}
|
||||
|
||||
tv_list_append_tv(l, &tv);
|
||||
}
|
||||
|
||||
/// Create the stacktrace from exestack.
|
||||
list_T *stacktrace_create(void)
|
||||
{
|
||||
list_T *const l = tv_list_alloc(exestack.ga_len);
|
||||
|
||||
for (int i = 0; i < exestack.ga_len; i++) {
|
||||
estack_T *const entry = &((estack_T *)exestack.ga_data)[i];
|
||||
linenr_T lnum = entry->es_lnum;
|
||||
|
||||
if (entry->es_type == ETYPE_SCRIPT) {
|
||||
stacktrace_push_item(l, NULL, NULL, lnum, entry->es_name, false);
|
||||
} else if (entry->es_type == ETYPE_UFUNC) {
|
||||
ufunc_T *const fp = entry->es_info.ufunc;
|
||||
const sctx_T sctx = fp->uf_script_ctx;
|
||||
bool filepath_alloced = false;
|
||||
char *filepath = sctx.sc_sid > 0
|
||||
? get_scriptname((LastSet){ .script_ctx = sctx },
|
||||
&filepath_alloced) : "";
|
||||
lnum += sctx.sc_lnum;
|
||||
stacktrace_push_item(l, fp, NULL, lnum, filepath, filepath_alloced);
|
||||
} else if (entry->es_type == ETYPE_AUCMD) {
|
||||
const sctx_T sctx = entry->es_info.aucmd->script_ctx;
|
||||
bool filepath_alloced = false;
|
||||
char *filepath = sctx.sc_sid > 0
|
||||
? get_scriptname((LastSet){ .script_ctx = sctx },
|
||||
&filepath_alloced) : "";
|
||||
lnum += sctx.sc_lnum;
|
||||
stacktrace_push_item(l, NULL, entry->es_name, lnum, filepath, filepath_alloced);
|
||||
}
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
/// getstacktrace() function
|
||||
void f_getstacktrace(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
||||
{
|
||||
tv_list_set_ret(rettv, stacktrace_create());
|
||||
}
|
||||
|
||||
static bool runtime_search_path_valid = false;
|
||||
static int *runtime_search_path_ref = NULL;
|
||||
static RuntimeSearchPath runtime_search_path;
|
||||
|
@ -220,7 +220,8 @@ M.vars = {
|
||||
type = 'string',
|
||||
desc = [=[
|
||||
The value of the exception most recently caught and not
|
||||
finished. See also |v:throwpoint| and |throw-variables|.
|
||||
finished. See also |v:stacktrace|, |v:throwpoint|, and
|
||||
|throw-variables|.
|
||||
Example: >vim
|
||||
try
|
||||
throw "oops"
|
||||
@ -701,6 +702,15 @@ M.vars = {
|
||||
<
|
||||
]=],
|
||||
},
|
||||
stacktrace = {
|
||||
type = 'table[]',
|
||||
desc = [=[
|
||||
The stack trace of the exception most recently caught and
|
||||
not finished. Refer to |getstacktrace()| for the structure of
|
||||
stack trace. See also |v:exception|, |v:throwpoint|, and
|
||||
|throw-variables|.
|
||||
]=],
|
||||
},
|
||||
statusmsg = {
|
||||
type = 'string',
|
||||
desc = [=[
|
||||
@ -823,7 +833,7 @@ M.vars = {
|
||||
desc = [=[
|
||||
The point where the exception most recently caught and not
|
||||
finished was thrown. Not set when commands are typed. See
|
||||
also |v:exception| and |throw-variables|.
|
||||
also |v:exception|, |v:stacktrace|, and |throw-variables|.
|
||||
Example: >vim
|
||||
try
|
||||
throw "oops"
|
||||
|
107
test/old/testdir/test_stacktrace.vim
Normal file
107
test/old/testdir/test_stacktrace.vim
Normal file
@ -0,0 +1,107 @@
|
||||
" Test for getstacktrace() and v:stacktrace
|
||||
|
||||
let s:thisfile = expand('%:p')
|
||||
let s:testdir = s:thisfile->fnamemodify(':h')
|
||||
|
||||
func Filepath(name)
|
||||
return s:testdir .. '/' .. a:name
|
||||
endfunc
|
||||
|
||||
func AssertStacktrace(expect, actual)
|
||||
call assert_equal(#{lnum: 581, filepath: Filepath('runtest.vim')}, a:actual[0])
|
||||
call assert_equal(a:expect, a:actual[-len(a:expect):])
|
||||
endfunc
|
||||
|
||||
func Test_getstacktrace()
|
||||
let g:stacktrace = []
|
||||
let lines1 =<< trim [SCRIPT]
|
||||
" Xscript1
|
||||
source Xscript2
|
||||
func Xfunc1()
|
||||
" Xfunc1
|
||||
call Xfunc2()
|
||||
endfunc
|
||||
[SCRIPT]
|
||||
let lines2 =<< trim [SCRIPT]
|
||||
" Xscript2
|
||||
func Xfunc2()
|
||||
" Xfunc2
|
||||
let g:stacktrace = getstacktrace()
|
||||
endfunc
|
||||
[SCRIPT]
|
||||
call writefile(lines1, 'Xscript1', 'D')
|
||||
call writefile(lines2, 'Xscript2', 'D')
|
||||
source Xscript1
|
||||
call Xfunc1()
|
||||
call AssertStacktrace([
|
||||
\ #{funcref: funcref('Test_getstacktrace'), lnum: 35, filepath: s:thisfile},
|
||||
\ #{funcref: funcref('Xfunc1'), lnum: 5, filepath: Filepath('Xscript1')},
|
||||
\ #{funcref: funcref('Xfunc2'), lnum: 4, filepath: Filepath('Xscript2')},
|
||||
\ ], g:stacktrace)
|
||||
unlet g:stacktrace
|
||||
endfunc
|
||||
|
||||
func Test_getstacktrace_event()
|
||||
let g:stacktrace = []
|
||||
let lines1 =<< trim [SCRIPT]
|
||||
" Xscript1
|
||||
func Xfunc()
|
||||
" Xfunc
|
||||
let g:stacktrace = getstacktrace()
|
||||
endfunc
|
||||
augroup test_stacktrace
|
||||
autocmd SourcePre * call Xfunc()
|
||||
augroup END
|
||||
[SCRIPT]
|
||||
let lines2 =<< trim [SCRIPT]
|
||||
" Xscript2
|
||||
[SCRIPT]
|
||||
call writefile(lines1, 'Xscript1', 'D')
|
||||
call writefile(lines2, 'Xscript2', 'D')
|
||||
source Xscript1
|
||||
source Xscript2
|
||||
call AssertStacktrace([
|
||||
\ #{funcref: funcref('Test_getstacktrace_event'), lnum: 62, filepath: s:thisfile},
|
||||
\ #{event: 'SourcePre Autocommands for "*"', lnum: 7, filepath: Filepath('Xscript1')},
|
||||
\ #{funcref: funcref('Xfunc'), lnum: 4, filepath: Filepath('Xscript1')},
|
||||
\ ], g:stacktrace)
|
||||
augroup test_stacktrace
|
||||
autocmd!
|
||||
augroup END
|
||||
unlet g:stacktrace
|
||||
endfunc
|
||||
|
||||
func Test_vstacktrace()
|
||||
let lines1 =<< trim [SCRIPT]
|
||||
" Xscript1
|
||||
source Xscript2
|
||||
func Xfunc1()
|
||||
" Xfunc1
|
||||
call Xfunc2()
|
||||
endfunc
|
||||
[SCRIPT]
|
||||
let lines2 =<< trim [SCRIPT]
|
||||
" Xscript2
|
||||
func Xfunc2()
|
||||
" Xfunc2
|
||||
throw 'Exception from Xfunc2'
|
||||
endfunc
|
||||
[SCRIPT]
|
||||
call writefile(lines1, 'Xscript1', 'D')
|
||||
call writefile(lines2, 'Xscript2', 'D')
|
||||
source Xscript1
|
||||
call assert_equal([], v:stacktrace)
|
||||
try
|
||||
call Xfunc1()
|
||||
catch
|
||||
let stacktrace = v:stacktrace
|
||||
endtry
|
||||
call assert_equal([], v:stacktrace)
|
||||
call AssertStacktrace([
|
||||
\ #{funcref: funcref('Test_vstacktrace'), lnum: 95, filepath: s:thisfile},
|
||||
\ #{funcref: funcref('Xfunc1'), lnum: 5, filepath: Filepath('Xscript1')},
|
||||
\ #{funcref: funcref('Xfunc2'), lnum: 4, filepath: Filepath('Xscript2')},
|
||||
\ ], stacktrace)
|
||||
endfunc
|
||||
|
||||
" vim: shiftwidth=2 sts=2 expandtab
|
Loading…
Reference in New Issue
Block a user