diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 8f30ac7c8f..9ce1786fa0 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -151,7 +151,7 @@ bool try_end(Error *err) if (should_free) { xfree(msg); } - } else if (did_throw) { + } else if (did_throw || need_rethrow) { if (*current_exception->throw_name != NUL) { if (current_exception->throw_lnum != 0) { api_set_error(err, kErrorTypeException, "%s, line %" PRIdLINENR ": %s", diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 7c3eb47247..12e746d49e 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -309,6 +309,33 @@ static void msg_verbose_cmd(linenr_T lnum, char *cmd) no_wait_return--; } +static int cmdline_call_depth = 0; ///< recursiveness + +/// Start executing an Ex command line. +/// +/// @return FAIL if too recursive, OK otherwise. +static int do_cmdline_start(void) +{ + assert(cmdline_call_depth >= 0); + // It's possible to create an endless loop with ":execute", catch that + // here. The value of 200 allows nested function calls, ":source", etc. + // Allow 200 or 'maxfuncdepth', whatever is larger. + if (cmdline_call_depth >= 200 && cmdline_call_depth >= p_mfd) { + return FAIL; + } + cmdline_call_depth++; + start_batch_changes(); + return OK; +} + +/// End executing an Ex command line. +static void do_cmdline_end(void) +{ + cmdline_call_depth--; + assert(cmdline_call_depth >= 0); + end_batch_changes(); +} + /// Execute a simple command line. Used for translated commands like "*". int do_cmdline_cmd(const char *cmd) { @@ -359,7 +386,6 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags) char *(*cmd_getline)(int, void *, int, bool); void *cmd_cookie; struct loop_cookie cmd_loop_cookie; - static int call_depth = 0; // recursiveness // For every pair of do_cmdline()/do_one_cmd() calls, use an extra memory // location for storing error messages to be converted to an exception. @@ -371,10 +397,7 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags) msg_list = &private_msg_list; private_msg_list = NULL; - // It's possible to create an endless loop with ":execute", catch that - // here. The value of 200 allows nested function calls, ":source", etc. - // Allow 200 or 'maxfuncdepth', whatever is larger. - if (call_depth >= 200 && call_depth >= p_mfd) { + if (do_cmdline_start() == FAIL) { emsg(_(e_command_too_recursive)); // When converting to an exception, we do not include the command name // since this is not an error of the specific command. @@ -382,8 +405,6 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags) msg_list = saved_msg_list; return FAIL; } - call_depth++; - start_batch_changes(); ga_init(&lines_ga, (int)sizeof(wcmd_T), 10); @@ -884,8 +905,7 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags) did_endif = false; // in case do_cmdline used recursively - call_depth--; - end_batch_changes(); + do_cmdline_end(); return retval; } @@ -1669,9 +1689,13 @@ static int execute_cmd0(int *retv, exarg_T *eap, const char **errormsg, bool pre /// @param preview Execute command preview callback instead of actual command int execute_cmd(exarg_T *eap, CmdParseInfo *cmdinfo, bool preview) { - const char *errormsg = NULL; int retv = 0; + if (do_cmdline_start() == FAIL) { + emsg(_(e_command_too_recursive)); + return retv; + } + const char *errormsg = NULL; #undef ERROR #define ERROR(msg) \ do { \ @@ -1738,9 +1762,12 @@ end: if (errormsg != NULL && *errormsg != NUL) { emsg(errormsg); } + // Undo command modifiers undo_cmdmod(&cmdmod); cmdmod = save_cmdmod; + + do_cmdline_end(); return retv; #undef ERROR } diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 91f61b5053..5cf48412a8 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -399,6 +399,19 @@ describe('API', function() ]], } end) + + it('errors properly when command too recursive', function() + exec_lua([[ + _G.success = false + vim.api.nvim_create_user_command('Test', function() + vim.api.nvim_exec2('Test', {}) + _G.success = true + end, {}) + ]]) + pcall_err(command, 'Test') + assert_alive() + eq(false, exec_lua('return _G.success')) + end) end) describe('nvim_command', function() @@ -4560,6 +4573,7 @@ describe('API', function() line6 ]] end) + it('works with count', function() insert [[ line1 @@ -4577,6 +4591,7 @@ describe('API', function() line6 ]] end) + it('works with register', function() insert [[ line1 @@ -4599,11 +4614,13 @@ describe('API', function() line6 ]] end) + it('works with bang', function() api.nvim_create_user_command('Foo', 'echo ""', { bang = true }) eq('!', api.nvim_cmd({ cmd = 'Foo', bang = true }, { output = true })) eq('', api.nvim_cmd({ cmd = 'Foo', bang = false }, { output = true })) end) + it('works with modifiers', function() -- with silent = true output is still captured eq( @@ -4659,6 +4676,7 @@ describe('API', function() feed(':call') eq('E471: Argument required', api.nvim_cmd({ cmd = 'messages' }, { output = true })) end) + it('works with magic.file', function() exec_lua([[ vim.api.nvim_create_user_command("Foo", function(opts) @@ -4673,6 +4691,7 @@ describe('API', function() ) ) end) + it('splits arguments correctly', function() exec([[ function! FooFunc(...) @@ -4695,6 +4714,7 @@ describe('API', function() ) ) end) + it('splits arguments correctly for Lua callback', function() api.nvim_exec_lua( [[ @@ -4721,6 +4741,7 @@ describe('API', function() ) ) end) + it('works with buffer names', function() command('edit foo.txt | edit bar.txt') api.nvim_cmd({ cmd = 'buffer', args = { 'foo.txt' } }, {}) @@ -4728,6 +4749,7 @@ describe('API', function() api.nvim_cmd({ cmd = 'buffer', args = { 'bar.txt' } }, {}) eq('bar.txt', fn.fnamemodify(api.nvim_buf_get_name(0), ':t')) end) + it('triggers CmdUndefined event if command is not found', function() api.nvim_exec_lua( [[ @@ -4742,13 +4764,16 @@ describe('API', function() ) eq('foo', api.nvim_cmd({ cmd = 'Foo' }, { output = true })) end) + it('errors if command is not implemented', function() eq('Command not implemented: winpos', pcall_err(api.nvim_cmd, { cmd = 'winpos' }, {})) end) + it('works with empty arguments list', function() api.nvim_cmd({ cmd = 'update' }, {}) api.nvim_cmd({ cmd = 'buffer', count = 0 }, {}) end) + it("doesn't suppress errors when used in keymapping", function() api.nvim_exec_lua( [[ @@ -4760,6 +4785,7 @@ describe('API', function() feed('[l') neq(nil, string.find(eval('v:errmsg'), 'E5108:')) end) + it('handles 0 range #19608', function() api.nvim_buf_set_lines(0, 0, -1, false, { 'aa' }) api.nvim_cmd({ cmd = 'delete', range = { 0 } }, {}) @@ -4767,12 +4793,14 @@ describe('API', function() eq({ 'aa' }, api.nvim_buf_get_lines(0, 0, 1, false)) assert_alive() end) + it('supports filename expansion', function() api.nvim_cmd({ cmd = 'argadd', args = { '%:p:h:t', '%:p:h:t' } }, {}) local arg = fn.expand('%:p:h:t') eq({ arg, arg }, fn.argv()) end) - it("'make' command works when argument count isn't 1 #19696", function() + + it(":make command works when argument count isn't 1 #19696", function() command('set makeprg=echo') command('set shellquote=') matches('^:!echo ', api.nvim_cmd({ cmd = 'make' }, { output = true })) @@ -4789,6 +4817,7 @@ describe('API', function() ) assert_alive() end) + it("doesn't display messages when output=true", function() local screen = Screen.new(40, 6) screen:attach() @@ -4817,31 +4846,37 @@ describe('API', function() ]], } end) + it('works with non-String args', function() eq('2', api.nvim_cmd({ cmd = 'echo', args = { 2 } }, { output = true })) eq('1', api.nvim_cmd({ cmd = 'echo', args = { true } }, { output = true })) end) + describe('first argument as count', function() it('works', function() command('vsplit | enew') api.nvim_cmd({ cmd = 'bdelete', args = { api.nvim_get_current_buf() } }, {}) eq(1, api.nvim_get_current_buf()) end) + it('works with :sleep using milliseconds', function() local start = uv.now() api.nvim_cmd({ cmd = 'sleep', args = { '100m' } }, {}) ok(uv.now() - start <= 300) end) end) + it(':call with unknown function does not crash #26289', function() eq( 'Vim:E117: Unknown function: UnknownFunc', pcall_err(api.nvim_cmd, { cmd = 'call', args = { 'UnknownFunc()' } }, {}) ) end) + it(':throw does not crash #24556', function() eq('42', pcall_err(api.nvim_cmd, { cmd = 'throw', args = { '42' } }, {})) end) + it('can use :return #24556', function() exec([[ func Foo() @@ -4854,5 +4889,18 @@ describe('API', function() eq('before', api.nvim_get_var('pos')) eq({ 1, 2, 3 }, api.nvim_get_var('result')) end) + + it('errors properly when command too recursive #27210', function() + exec_lua([[ + _G.success = false + vim.api.nvim_create_user_command('Test', function() + vim.api.nvim_cmd({ cmd = 'Test' }, {}) + _G.success = true + end, {}) + ]]) + pcall_err(command, 'Test') + assert_alive() + eq(false, exec_lua('return _G.success')) + end) end) end)