patch 9.2.1140: Vim9: :def in legacy script can create a script variable

Problem:  A :def function in a legacy script can create a script-local
          variable by assigning to it, while declaring one with "var"
          gives E1101.
Solution: Give E1089 for assigning to an unknown script-local variable
          in any :def function, not only in one defined in a Vim9
          script.  Remove uf_script_ctx_version, which was only used
          for this check.  Do not give it with ":source ++dryrun".

runtime/menu.vim created s:bmenu_short, s:bmenu_count, s:suglist,
s:fromword, s:changeitem, s:additem and s:ignoreitem this way, define
them at the script level.

":source ++dryrun" does not execute ":let" in a legacy script, thus
every script variable would be unknown when compiling.  Document that
reading or calling such a variable without "s:" still gives an error,
and separate the paragraphs of ":source-dryrun" with blank lines.

Test_unlet() created s:somevar this way, define it at the script level
instead.  Test_using_s_var_in_function() checks that assigning to
s:scriptlevel in a :def function of a legacy script gives E1089 when
the variable does not exist.  Test_source_dryrun() checks that
assigning to a script variable with "s:" in a :def function of a
legacy script gives no error with ":source ++dryrun".

closes: #21379

Assisted-by: Claude
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Hirohito Higashi authored and Christian Brabandt committed 2026-09-28 19:22:15 +00:00
1 parent c77608d35e
commit 6175ce8762
10 files changed
+57 -11

No files matched your search

+12 -1
View File
@@ -1,4 +1,4 @@
*repeat.txt* For Vim version 9.2. Last change: 2026 Sep 25
*repeat.txt* For Vim version 9.2. Last change: 2026 Sep 28
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -248,6 +248,7 @@ For writing a Vim script, see chapter 41 of the user manual |usr_41.txt|.
Read the script without running it: only what defines
something is executed, then every |:def| function in
it is compiled and the errors are reported as usual.
What is executed: `vim9script`, `import`, `def`,
`function`, `class`, `interface`, `enum`, `type` and a
declaration with `var`, `const` or `final`. The
@@ -256,17 +257,27 @@ For writing a Vim script, see chapter 41 of the user manual |usr_41.txt|.
expression is not evaluated. A static class variable
is treated the same way and an enum value is not
created.
Everything else is skipped, also `finish` and the
condition of an `if`, `while` or `for`; a definition
inside such a block is executed whether the condition
holds or not. A function may therefore be defined
twice, the last definition counts.
In a legacy script `let` is not executed, thus a
script variable created by it does not exist. A
|:def| function can use it with "s:", but reading it
or calling it without "s:" gives an error. Defining a
function in a Dictionary it holds, such as
`function s:obj.Method()`, fails as well.
A script that is imported is read the same way, but an
autoload script is not loaded. No |SourceCmd|,
|SourcePre| or |SourcePost| autocommand is triggered.
A class or enum in an autoload script is defined again
by a dry run, and after one by a normal load, since no
object was made from it; see |vim9-reload|.
Note: An error in a definition does not stop the
reading, the rest of the script is read as well. And
an error in a line of a function does not stop
+3 -1
View File
@@ -1,4 +1,4 @@
*version9.txt* For Vim version 9.2. Last change: 2026 Sep 25
*version9.txt* For Vim version 9.2. Last change: 2026 Sep 28
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -52719,6 +52719,8 @@ Changed ~
- The highlighting groups |hl-CursorLineFold| and |hl-CursorLineSign| are
always used when 'cursorline' is set.
- Virtual text above a line is now counted by |screenpos()| and |virtcol()|.
- A |:def| function in a legacy script can no longer create a script-local
variable by assigning to it, this gives |E1089|.
*added-9.3*
Added ~
+4 -2
View File
@@ -1,4 +1,4 @@
*vim9.txt* For Vim version 9.2. Last change: 2026 Sep 25
*vim9.txt* For Vim version 9.2. Last change: 2026 Sep 28
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -3610,7 +3610,9 @@ or within a `:function`. The three rules are:
regardless of the function's parent context. Similarly, it gives E1268 in
the script-local scope of a Vim9 script.
3. In a legacy Vim script, "s:" is optional in a `:def` function, regardless
of the function's parent context.
of the function's parent context. A `:def` function cannot create a
script-local variable, assigning to one that does not exist when the
function is compiled gives |E1089|.
The following three scripts demonstrate these rules:
>vim
+9 -1
View File
@@ -2,7 +2,7 @@
" You can also use this as a start for your own set of menus.
"
" Maintainer: The Vim Project <https://github.com/vim/vim>
" Last Change: 2026 Jan 19
" Last Change: 2026 Sep 28
" Former Maintainer: Bram Moolenaar <Bram@vim.org>
" Note that ":an" (short for ":anoremenu") is often used to make a menu work
@@ -677,6 +677,8 @@ if !exists("no_buffers_menu")
" wait with building the menu until after loading 'session' files. Makes
" startup faster.
let s:bmenu_wait = 1
let s:bmenu_short = 1
let s:bmenu_count = 0
" Dictionary of buffer number to name. This helps prevent problems where a
" buffer as renamed and we didn't keep track of that.
@@ -1008,6 +1010,12 @@ cnoremenu <script> <silent> 1.100 PopUp.Select\ &All <C-U>call <SID>SelectAll()<
if has("spell")
" Spell suggestions in the popup menu. Note that this will slow down the
" appearance of the menu!
let s:suglist = []
let s:fromword = ''
let s:changeitem = ''
let s:additem = ''
let s:ignoreitem = ''
def s:SpellPopup()
if exists("s:changeitem") && s:changeitem != ''
call s:SpellDel()
-1
View File
@@ -2083,7 +2083,6 @@ struct ufunc_S
sctx_T uf_script_ctx; // SCTX where function was defined,
// used for s: variables; sc_version changed
// for :function
int uf_script_ctx_version; // original sc_version of SCTX
int uf_refcount; // reference count, see func_name_refcount()
funccall_T *uf_scoped; // l: local variables for closure
+13
View File
@@ -859,6 +859,19 @@ func Test_source_dryrun()
call assert_true(exists('*DryrunLegacy'))
delfunc DryrunLegacy
" a :def function in a legacy script can assign to a script variable
" with "s:", although the ":let" that creates it is not executed
let lines =<< trim END
let s:count = 0
def DryrunLegacyDef()
s:count += 1
enddef
END
call writefile(lines, 'Xdryrun_legacydef.vim', 'D')
source ++dryrun Xdryrun_legacydef.vim
call assert_true(exists('*DryrunLegacyDef'))
delfunc DryrunLegacyDef
" no SourceCmd, SourcePre or SourcePost autocommand, also not for the
" imported script
let g:dryrun_events = ''
+11
View File
@@ -2682,6 +2682,8 @@ def Test_cannot_use_let()
v9.CheckDefAndScriptFailure(['let a = 34'], 'E1126:', 1)
enddef
let s:somevar = ''
def Test_unlet()
g:somevar = 'yes'
assert_true(exists('g:somevar'))
@@ -3141,6 +3143,15 @@ def Test_using_s_var_in_function()
call assert_equal(456, s:scriptlevel)
END
v9.CheckScriptSuccess(lines)
# but a :def function cannot create one
lines =<< trim END
def s:SomeFunc()
s:scriptlevel = 456
enddef
call s:SomeFunc()
END
v9.CheckScriptFailure(lines, 'E1089: Unknown variable: s:scriptlevel', 1)
enddef
" Test for specifying a type in assignment
-2
View File
@@ -1624,7 +1624,6 @@ lambda_function_body(
if (!ASCII_ISUPPER(*ufunc->uf_name))
ufunc->uf_flags |= FC_VIM9;
ufunc->uf_script_ctx = current_sctx;
ufunc->uf_script_ctx_version = current_sctx.sc_version;
ufunc->uf_script_ctx.sc_lnum += sourcing_lnum_top;
set_function_type(ufunc);
@@ -5739,7 +5738,6 @@ define_function(
fp->uf_calls = 0;
fp->uf_cleared = FALSE;
fp->uf_script_ctx = current_sctx;
fp->uf_script_ctx_version = current_sctx.sc_version;
fp->uf_script_ctx.sc_lnum += sourcing_lnum_top;
if (is_export)
{
+2
View File
@@ -763,6 +763,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
1140,
/**/
1139,
/**/
+3 -3
View File
@@ -1905,9 +1905,9 @@ compile_lhs_script_var(
lhs->lhs_name);
return FAIL;
}
else if (cctx->ctx_ufunc->uf_script_ctx_version == SCRIPT_VERSION_VIM9
&& script_namespace
&& !script_var && import == NULL)
// With ":source ++dryrun" the ":let" that creates it was not executed.
else if (script_namespace && !script_var && import == NULL
&& !source_dryrun)
{
semsg(_(e_unknown_variable_str), lhs->lhs_name);
return FAIL;