neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit b64e36cef099dae9cf3a8b21774783e28d17d51d
parent 62de643b897caee04e6fdfd89388054bd6ae9794
Author: zeertzjq <zeertzjq@outlook.com>
Date:   Tue,  2 Dec 2025 06:50:30 +0800

vim-patch:9.1.1943: Memory leak with :breakadd expr

Problem:  Memory leak with :breakadd expr
Solution: Free debug_oldval and debug_newval before assigning to them.
          Verify the existing (though confusing) :breakadd expr behavior
          (zeertzjq).

It seems that :breakadd expr doesn't work as documented at all. This PR
only fixes the memory leak. The tests are for the existing behavior.

closes: vim/vim#18844

https://github.com/vim/vim/commit/a474de64dfd853eb5d3bf04f8bbd6f7d16e10c82

Diffstat:
Msrc/nvim/debugger.c | 6++++++
Mtest/functional/legacy/debugger_spec.lua | 130++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mtest/old/testdir/test_debugger.vim | 55+++++++++++++++++++++++++++++++++++++++++++++++--------
3 files changed, 142 insertions(+), 49 deletions(-)

diff --git a/src/nvim/debugger.c b/src/nvim/debugger.c @@ -828,17 +828,21 @@ static linenr_T debuggy_find(bool file, char *fname, linenr_T after, garray_T *g typval_T *const tv = eval_expr_no_emsg(bp); if (tv != NULL) { if (bp->dbg_val == NULL) { + xfree(debug_oldval); debug_oldval = typval_tostring(NULL, true); bp->dbg_val = tv; + xfree(debug_newval); debug_newval = typval_tostring(bp->dbg_val, true); line = true; } else { if (typval_compare(tv, bp->dbg_val, EXPR_IS, false) == OK && tv->vval.v_number == false) { line = true; + xfree(debug_oldval); debug_oldval = typval_tostring(bp->dbg_val, true); // Need to evaluate again, typval_compare() overwrites "tv". typval_T *const v = eval_expr_no_emsg(bp); + xfree(debug_newval); debug_newval = typval_tostring(v, true); tv_free(bp->dbg_val); bp->dbg_val = v; @@ -846,7 +850,9 @@ static linenr_T debuggy_find(bool file, char *fname, linenr_T after, garray_T *g tv_free(tv); } } else if (bp->dbg_val != NULL) { + xfree(debug_oldval); debug_oldval = typval_tostring(bp->dbg_val, true); + xfree(debug_newval); debug_newval = typval_tostring(NULL, true); tv_free(bp->dbg_val); bp->dbg_val = NULL; diff --git a/test/functional/legacy/debugger_spec.lua b/test/functional/legacy/debugger_spec.lua @@ -13,64 +13,112 @@ describe('debugger', function() local screen before_each(function() - screen = Screen.new(999, 10) + screen = Screen.new(999, 7) end) -- oldtest: Test_Debugger_breakadd_expr() + -- This doesn't seem to work as documented. The breakpoint is not + -- triggered until the next function call. it(':breakadd expr', function() - write_file('XdebugBreakExpr.vim', 'let g:Xtest_var += 1') + write_file( + 'XbreakExpr.vim', + [[ + func Foo() + eval 1 + eval 2 + endfunc + + let g:Xtest_var += 1 + call Foo() + let g:Xtest_var += 1 + call Foo()]] + ) finally(function() - os.remove('XdebugBreakExpr.vim') + os.remove('XbreakExpr.vim') end) - command('edit XdebugBreakExpr.vim') + command('edit XbreakExpr.vim') command(':let g:Xtest_var = 10') command(':breakadd expr g:Xtest_var') - feed(':source %<CR>') - screen:expect { - grid = [[ - ^let g:Xtest_var += 1{MATCH: *}| - {1:~{MATCH: *}}|*8 - :source %{MATCH: *}| - ]], - } - feed(':source %<CR>') - screen:expect { - grid = [[ + local initial_screen = [[ + ^func Foo(){MATCH: *}| + eval 1{MATCH: *}| + eval 2{MATCH: *}| + endfunc{MATCH: *}| + {MATCH: *}| let g:Xtest_var += 1{MATCH: *}| - {1:~{MATCH: *}}| - {3:{MATCH: *}}| - Breakpoint in "{MATCH:.*}XdebugBreakExpr.vim" line 1{MATCH: *}| + {MATCH: *}| + ]] + screen:expect(initial_screen) + + feed(':source %<CR>') + screen:expect([[ + Breakpoint in "Foo" line 1{MATCH: *}| Entering Debug mode. Type "cont" to continue.{MATCH: *}| Oldval = "10"{MATCH: *}| Newval = "11"{MATCH: *}| - {MATCH:.*}XdebugBreakExpr.vim{MATCH: *}| - line 1: let g:Xtest_var += 1{MATCH: *}| + {MATCH:.*}XbreakExpr.vim[7]..function Foo{MATCH: *}| + line 1: eval 1{MATCH: *}| >^{MATCH: *}| - ]], - } + ]]) feed('cont<CR>') - screen:expect { - grid = [[ - ^let g:Xtest_var += 1{MATCH: *}| - {1:~{MATCH: *}}|*8 - {MATCH: *}| - ]], - } - feed(':source %<CR>') - screen:expect { - grid = [[ - let g:Xtest_var += 1{MATCH: *}| - {1:~{MATCH: *}}| - {3:{MATCH: *}}| - Breakpoint in "{MATCH:.*}XdebugBreakExpr.vim" line 1{MATCH: *}| - Entering Debug mode. Type "cont" to continue.{MATCH: *}| + screen:expect([[ + >cont{MATCH: *}| + Breakpoint in "Foo" line 1{MATCH: *}| Oldval = "11"{MATCH: *}| Newval = "12"{MATCH: *}| - {MATCH:.*}XdebugBreakExpr.vim{MATCH: *}| - line 1: let g:Xtest_var += 1{MATCH: *}| + {MATCH:.*}XbreakExpr.vim[9]..function Foo{MATCH: *}| + line 1: eval 1{MATCH: *}| >^{MATCH: *}| - ]], - } + ]]) + feed('cont<CR>') + screen:expect(initial_screen) + + -- Check the behavior without the g: prefix. + -- The Oldval and Newval don't look right here. + command(':breakdel *') + command(':breakadd expr Xtest_var') + feed(':source %<CR>') + screen:expect([[ + Breakpoint in "Foo" line 1{MATCH: *}| + Entering Debug mode. Type "cont" to continue.{MATCH: *}| + Oldval = "13"{MATCH: *}| + Newval = "(does not exist)"{MATCH: *}| + {MATCH:.*}XbreakExpr.vim[7]..function Foo{MATCH: *}| + line 1: eval 1{MATCH: *}| + >^{MATCH: *}| + ]]) + feed('cont<CR>') + screen:expect([[ + {MATCH:.*}XbreakExpr.vim[7]..function Foo{MATCH: *}| + line 1: eval 1{MATCH: *}| + >cont{MATCH: *}| + Breakpoint in "Foo" line 2{MATCH: *}| + {MATCH:.*}XbreakExpr.vim[7]..function Foo{MATCH: *}| + line 2: eval 2{MATCH: *}| + >^{MATCH: *}| + ]]) + feed('cont<CR>') + screen:expect([[ + >cont{MATCH: *}| + Breakpoint in "Foo" line 1{MATCH: *}| + Oldval = "14"{MATCH: *}| + Newval = "(does not exist)"{MATCH: *}| + {MATCH:.*}XbreakExpr.vim[9]..function Foo{MATCH: *}| + line 1: eval 1{MATCH: *}| + >^{MATCH: *}| + ]]) + feed('cont<CR>') + screen:expect([[ + {MATCH:.*}XbreakExpr.vim[9]..function Foo{MATCH: *}| + line 1: eval 1{MATCH: *}| + >cont{MATCH: *}| + Breakpoint in "Foo" line 2{MATCH: *}| + {MATCH:.*}XbreakExpr.vim[9]..function Foo{MATCH: *}| + line 2: eval 2{MATCH: *}| + >^{MATCH: *}| + ]]) + feed('cont<CR>') + screen:expect(initial_screen) end) end) diff --git a/test/old/testdir/test_debugger.vim b/test/old/testdir/test_debugger.vim @@ -364,35 +364,74 @@ func Test_Debugger_breakadd() endfunc " Test for expression breakpoint set using ":breakadd expr <expr>" +" FIXME: This doesn't seem to work as documented. The breakpoint is not +" triggered until the next function call. func Test_Debugger_breakadd_expr() CheckRunVimInTerminal CheckCWD let lines =<< trim END + func Foo() + eval 1 + eval 2 + endfunc + let g:Xtest_var += 1 + call Foo() + let g:Xtest_var += 1 + call Foo() END - call writefile(lines, 'XdebugBreakExpr.vim', 'D') + call writefile(lines, 'XbreakExpr.vim', 'D') " Start Vim in a terminal - let buf = RunVimInTerminal('XdebugBreakExpr.vim', {}) + let buf = RunVimInTerminal('XbreakExpr.vim', {}) call s:RunDbgCmd(buf, ':let g:Xtest_var = 10') call s:RunDbgCmd(buf, ':breakadd expr g:Xtest_var') - call s:RunDbgCmd(buf, ':source %') let expected =<< trim eval END Oldval = "10" Newval = "11" - {fnamemodify('XdebugBreakExpr.vim', ':p')} - line 1: let g:Xtest_var += 1 + {fnamemodify('XbreakExpr.vim', ':p')}[7]..function Foo + line 1: eval 1 END call s:RunDbgCmd(buf, ':source %', expected) - call s:RunDbgCmd(buf, 'cont') let expected =<< trim eval END Oldval = "11" Newval = "12" - {fnamemodify('XdebugBreakExpr.vim', ':p')} - line 1: let g:Xtest_var += 1 + {fnamemodify('XbreakExpr.vim', ':p')}[9]..function Foo + line 1: eval 1 + END + call s:RunDbgCmd(buf, 'cont', expected) + call s:RunDbgCmd(buf, 'cont') + + " Check the behavior without the g: prefix. + " FIXME: The Oldval and Newval don't look right here. + call s:RunDbgCmd(buf, ':breakdel *') + call s:RunDbgCmd(buf, ':breakadd expr Xtest_var') + let expected =<< trim eval END + Oldval = "13" + Newval = "(does not exist)" + {fnamemodify('XbreakExpr.vim', ':p')}[7]..function Foo + line 1: eval 1 END call s:RunDbgCmd(buf, ':source %', expected) + let expected =<< trim eval END + {fnamemodify('XbreakExpr.vim', ':p')}[7]..function Foo + line 2: eval 2 + END + call s:RunDbgCmd(buf, 'cont', expected) + let expected =<< trim eval END + Oldval = "14" + Newval = "(does not exist)" + {fnamemodify('XbreakExpr.vim', ':p')}[9]..function Foo + line 1: eval 1 + END + call s:RunDbgCmd(buf, 'cont', expected) + let expected =<< trim eval END + {fnamemodify('XbreakExpr.vim', ':p')}[9]..function Foo + line 2: eval 2 + END + call s:RunDbgCmd(buf, 'cont', expected) + call s:RunDbgCmd(buf, 'cont') call StopVimInTerminal(buf) endfunc