0
0
mirror of https://github.com/vim/vim.git synced 2025-08-25 19:53:53 -04:00

patch 9.1.0850: Vim9: cannot access nested object inside objects

Problem:  Vim9: cannot access nested object inside objects
          (lifepillar, 91khr, mawkish)
Solution: Add support for accessing an object member using a "any"
          variable type (Yegappan Lakshmanan)

fixes: #11822
fixes: #12024
fixes: #12196
fixes: #12198
closes: #16029

Signed-off-by: Yegappan Lakshmanan <yegappan@yahoo.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Yegappan Lakshmanan 2024-11-11 19:58:55 +01:00 committed by Christian Brabandt
parent 622f6f5b9a
commit 56d45f1b66
No known key found for this signature in database
GPG Key ID: F3F92DA383FDDE09
7 changed files with 523 additions and 46 deletions

View File

@ -9282,6 +9282,7 @@ o_CTRL-V motion.txt /*o_CTRL-V*
o_V motion.txt /*o_V*
o_object-select motion.txt /*o_object-select*
o_v motion.txt /*o_v*
obj-var-type-any vim9class.txt /*obj-var-type-any*
object vim9class.txt /*object*
object-const-variable vim9class.txt /*object-const-variable*
object-empty() vim9class.txt /*object-empty()*

View File

@ -1,4 +1,4 @@
*vim9class.txt* For Vim version 9.1. Last change: 2024 Apr 13
*vim9class.txt* For Vim version 9.1. Last change: 2024 Nov 11
VIM REFERENCE MANUAL by Bram Moolenaar
@ -873,6 +873,33 @@ Note that the method name must start with "new". If there is no method called
"new()" then the default constructor is added, even though there are other
constructor methods.
Using variable type "any" for an Object~
*obj-var-type-any*
You can use a variable declared with type "any" to hold an object. e.g.
>
vim9script
class A
var n = 10
def Get(): number
return this.n
enddef
endclass
def Fn(o: any)
echo o.n
echo o.Get()
enddef
var a = A.new()
Fn(a)
<
In this example, Vim cannot determine the type of the parameter "o" for
function Fn() at compile time. It can be either a |Dict| or an |Object|
value. Therefore, at runtime, when the type is known, the object member
variable and method are looked up. This process is not efficient, so it is
recommended to use a more specific type whenever possible for better
efficiency.
Compiling methods in a Class ~
*class-compile*
The |:defcompile| command can be used to compile all the class and object

View File

@ -9,6 +9,8 @@ type_T *oc_member_type_by_idx(class_T *cl, int is_object, int member_idx);
void ex_enum(exarg_T *eap);
void typealias_unref(typealias_T *ta);
void ex_type(exarg_T *eap);
int get_member_tv(class_T *cl, int is_object, char_u *name, size_t namelen, class_T *current_class, typval_T *rettv);
int obj_method_to_partial_tv(object_T *obj, ufunc_T *obj_method, typval_T *rettv);
int class_object_index(char_u **arg, typval_T *rettv, evalarg_T *evalarg, int verbose);
ufunc_T *find_class_func(char_u **arg);
int class_member_idx(class_T *cl, char_u *name, size_t namelen);

View File

@ -784,7 +784,7 @@ def Test_member_any_used_as_object()
vim9script
class Inner
var value: number = 0
public var value: number = 0
endclass
class Outer
@ -11213,4 +11213,339 @@ def Test_class_cast()
v9.CheckScriptSuccess(lines)
enddef
" Test for using a variable of type "any" with an object
def Test_any_obj_var_type()
var lines =<< trim END
vim9script
class A
var name: string = "foobar"
def Foo(): string
return "func foo"
enddef
endclass
def CheckVals(x: any)
assert_equal("foobar", x.name)
assert_equal("func foo", x.Foo())
enddef
var a = A.new()
CheckVals(a)
END
v9.CheckScriptSuccess(lines)
# Try to set a non-existing variable
lines =<< trim END
vim9script
class A
var name: string = "foobar"
endclass
def SetNonExistingVar(x: any)
x.bar = [1, 2, 3]
enddef
var a = A.new()
SetNonExistingVar(a)
END
v9.CheckScriptFailure(lines, 'E1326: Variable "bar" not found in object "A"', 1)
# Try to read a non-existing variable
lines =<< trim END
vim9script
class A
var name: string = "foobar"
endclass
def GetNonExistingVar(x: any)
var i: dict<any> = x.bar
enddef
var a = A.new()
GetNonExistingVar(a)
END
v9.CheckScriptFailure(lines, 'E1326: Variable "bar" not found in object "A"', 1)
# Try to invoke a non-existing method
lines =<< trim END
vim9script
class A
def Foo(): number
return 10
enddef
endclass
def CallNonExistingMethod(x: any)
var i: number = x.Bar()
enddef
var a = A.new()
CallNonExistingMethod(a)
END
v9.CheckScriptFailure(lines, 'E1326: Variable "Bar" not found in object "A"', 1)
# Use an object which is a Dict value
lines =<< trim END
vim9script
class Foo
def Bar(): number
return 369
enddef
endclass
def GetValue(FooDict: dict<any>): number
var n: number = 0
for foo in values(FooDict)
n += foo.Bar()
endfor
return n
enddef
var d = {'x': Foo.new()}
assert_equal(369, GetValue(d))
END
v9.CheckScriptSuccess(lines)
# Nested data. Object containg a Dict containing another Object.
lines =<< trim END
vim9script
class Context
public var state: dict<any> = {}
endclass
class Metadata
public var value = 0
endclass
var ctx = Context.new()
ctx.state["meta"] = Metadata.new(2468)
const foo = ctx.state.meta.value
def F(): number
const bar = ctx.state.meta.value
return bar
enddef
assert_equal(2468, F())
END
v9.CheckScriptSuccess(lines)
# Accessing an object from a method inside the class using any type
lines =<< trim END
vim9script
class C
def _G(): string
return '_G'
enddef
static def S(o_any: any): string
return o_any._G()
enddef
endclass
var o1 = C.new()
assert_equal('_G', C.S(o1))
END
v9.CheckScriptSuccess(lines)
# Modifying an object private variable from a method in another class using
# any type
lines =<< trim END
vim9script
class A
var num = 10
endclass
class B
def SetVal(x: any)
x.num = 20
enddef
endclass
var a = A.new()
var b = B.new()
b.SetVal(a)
END
v9.CheckScriptFailure(lines, 'E1335: Variable "num" in class "A" is not writable', 1)
# Accessing a object protected variable from a method in another class using
# any type
lines =<< trim END
vim9script
class A
var _num = 10
endclass
class B
def GetVal(x: any): number
return x._num
enddef
endclass
var a = A.new()
var b = B.new()
var i = b.GetVal(a)
END
v9.CheckScriptFailure(lines, 'E1333: Cannot access protected variable "_num" in class "A"', 1)
# Accessing an object returned from an imported function and class
lines =<< trim END
vim9script
export class Foo
public var name: string
endclass
export def ReturnFooObject(): Foo
var r = Foo.new('star')
return r
enddef
END
writefile(lines, 'Xanyvar1.vim', 'D')
lines =<< trim END
vim9script
import './Xanyvar1.vim'
def GetName(): string
var whatever = Xanyvar1.ReturnFooObject()
return whatever.name
enddef
assert_equal('star', GetName())
END
v9.CheckScriptSuccess(lines)
# Try to modify a private object variable using a variable of type "any"
lines =<< trim END
vim9script
class Foo
var n: number = 10
endclass
def Fn(x: any)
x.n = 20
enddef
var a = Foo.new()
Fn(a)
END
v9.CheckScriptFailure(lines, 'E1335: Variable "n" in class "Foo" is not writable', 1)
# Try to read a protected object variable using a variable of type "any"
lines =<< trim END
vim9script
class Foo
var _n: number = 10
endclass
def Fn(x: any): number
return x._n
enddef
var a = Foo.new()
Fn(a)
END
v9.CheckScriptFailure(lines, 'E1333: Cannot access protected variable "_n" in class "Foo"', 1)
# Read a protected object variable using a variable of type "any" in an object
# method
lines =<< trim END
vim9script
class Foo
var _n: number = 10
def Fn(x: any): number
return x._n
enddef
endclass
var a = Foo.new()
assert_equal(10, a.Fn(a))
END
v9.CheckScriptSuccess(lines)
# Try to call a protected object method using a "any" type variable
lines =<< trim END
vim9script
class Foo
def _GetVal(): number
return 234
enddef
endclass
def Fn(x: any): number
return x._GetVal()
enddef
var a = Foo.new()
Fn(a)
END
v9.CheckScriptFailure(lines, 'E1366: Cannot access protected method: _GetVal', 1)
# Call a protected object method using a "any" type variable from another
# object method
lines =<< trim END
vim9script
class Foo
def _GetVal(): number
return 234
enddef
def FooVal(x: any): number
return x._GetVal()
enddef
endclass
var a = Foo.new()
assert_equal(234, a.FooVal(a))
END
v9.CheckScriptSuccess(lines)
# Method chaining
lines =<< trim END
vim9script
export class T
var id: number = 268
def F(): any
return this
enddef
endclass
def H()
var a = T.new().F().F()
assert_equal(268, a.id)
enddef
H()
var b: T = T.new().F().F()
assert_equal(268, b.id)
END
v9.CheckScriptSuccess(lines)
# Using a null object to access a member variable
lines =<< trim END
vim9script
def Fn(x: any): number
return x.num
enddef
Fn(null_object)
END
v9.CheckScriptFailure(lines, 'E1360: Using a null object', 1)
# Using a null object to invoke a method
lines =<< trim END
vim9script
def Fn(x: any)
x.Foo()
enddef
Fn(null_object)
END
v9.CheckScriptFailure(lines, 'E1360: Using a null object', 1)
enddef
" vim: ts=8 sw=2 sts=2 expandtab tw=80 fdm=marker

View File

@ -704,6 +704,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
850,
/**/
849,
/**/

View File

@ -2777,12 +2777,13 @@ done:
* "rettv". If "is_object" is TRUE, then the object member variable table is
* searched. Otherwise the class member variable table is searched.
*/
static int
int
get_member_tv(
class_T *cl,
int is_object,
char_u *name,
size_t namelen,
class_T *current_class,
typval_T *rettv)
{
ocmember_T *m;
@ -2793,7 +2794,8 @@ get_member_tv(
if (m == NULL)
return FAIL;
if (*name == '_')
if (*name == '_' && (current_class == NULL ||
!class_instance_of(current_class, cl)))
{
emsg_var_cl_define(e_cannot_access_protected_variable_str,
m->ocm_name, 0, cl);
@ -2873,7 +2875,7 @@ call_oc_method(
if (ocm == NULL && *fp->uf_name == '_')
{
// Cannot access a private method outside of a class
// Cannot access a protected method outside of a class
semsg(_(e_cannot_access_protected_method_str), fp->uf_name);
return FAIL;
}
@ -2916,6 +2918,33 @@ call_oc_method(
return OK;
}
/*
* Create a partial typval for "obj.obj_method" and store it in "rettv".
* Returns OK on success and FAIL on memory allocation failure.
*/
int
obj_method_to_partial_tv(object_T *obj, ufunc_T *obj_method, typval_T *rettv)
{
partial_T *pt = ALLOC_CLEAR_ONE(partial_T);
if (pt == NULL)
return FAIL;
pt->pt_refcount = 1;
if (obj != NULL)
{
pt->pt_obj = obj;
++pt->pt_obj->obj_refcount;
}
pt->pt_auto = TRUE;
pt->pt_func = obj_method;
func_ptr_ref(pt->pt_func);
rettv->v_type = VAR_PARTIAL;
rettv->vval.v_partial = pt;
return OK;
}
/*
* Evaluate what comes after a class:
* - class member: SomeClass.varname
@ -2978,7 +3007,7 @@ class_object_index(
// Search in the object member variable table and the class member
// variable table.
int is_object = rettv->v_type == VAR_OBJECT;
if (get_member_tv(cl, is_object, name, len, rettv) == OK)
if (get_member_tv(cl, is_object, name, len, NULL, rettv) == OK)
{
*arg = name_end;
return OK;
@ -2989,28 +3018,17 @@ class_object_index(
ufunc_T *fp = method_lookup(cl, rettv->v_type, name, len, &fidx);
if (fp != NULL)
{
// Private methods are not accessible outside the class
// Protected methods are not accessible outside the class
if (*name == '_')
{
semsg(_(e_cannot_access_protected_method_str), fp->uf_name);
return FAIL;
}
partial_T *pt = ALLOC_CLEAR_ONE(partial_T);
if (pt == NULL)
if (obj_method_to_partial_tv(is_object ? rettv->vval.v_object :
NULL, fp, rettv) == FAIL)
return FAIL;
pt->pt_refcount = 1;
if (is_object)
{
pt->pt_obj = rettv->vval.v_object;
++pt->pt_obj->obj_refcount;
}
pt->pt_auto = TRUE;
pt->pt_func = fp;
func_ptr_ref(pt->pt_func);
rettv->v_type = VAR_PARTIAL;
rettv->vval.v_partial = pt;
*arg = name_end;
return OK;
}

View File

@ -2268,15 +2268,23 @@ execute_storeindex(isn_T *iptr, ectx_T *ectx)
ocmember_T *m = object_member_lookup(cl, member, 0, &m_idx);
if (m != NULL)
{
if (*member == '_')
// Get the current function
ufunc_T *ufunc = (((dfunc_T *)def_functions.ga_data)
+ ectx->ec_dfunc_idx)->df_ufunc;
// Check whether the member variable is writeable
if ((m->ocm_access != VIM_ACCESS_ALL) &&
(ufunc->uf_class == NULL ||
!class_instance_of(ufunc->uf_class, cl)))
{
emsg_var_cl_define(
e_cannot_access_protected_variable_str,
m->ocm_name, 0, cl);
char *msg = (m->ocm_access == VIM_ACCESS_PRIVATE)
? e_cannot_access_protected_variable_str
: e_variable_is_not_writable_str;
emsg_var_cl_define(msg, m->ocm_name, 0, cl);
status = FAIL;
}
lidx = m_idx;
else
lidx = m_idx;
}
else
{
@ -3119,6 +3127,73 @@ object_required_error(typval_T *tv)
clear_type_list(&type_list);
}
/*
* Accessing the member of an object stored in a variable of type "any".
* Returns OK if the member variable is present.
* Returns FAIL if the variable is not found.
*/
static int
any_var_get_obj_member(class_T *current_class, isn_T *iptr, typval_T *tv)
{
object_T *obj = tv->vval.v_object;
typval_T mtv;
if (obj == NULL)
{
SOURCING_LNUM = iptr->isn_lnum;
emsg(_(e_using_null_object));
return FAIL;
}
// get_member_tv() needs the object information in the typval argument.
// So set the object information.
copy_tv(tv, &mtv);
// 'name' can either be a object variable or a object method
int namelen = STRLEN(iptr->isn_arg.string);
int save_did_emsg = did_emsg;
if (get_member_tv(obj->obj_class, TRUE, iptr->isn_arg.string, namelen,
current_class, &mtv) == OK)
{
copy_tv(&mtv, tv);
clear_tv(&mtv);
return OK;
}
if (did_emsg != save_did_emsg)
return FAIL;
// could be a member function
ufunc_T *obj_method;
int obj_method_idx;
obj_method = method_lookup(obj->obj_class, VAR_OBJECT,
iptr->isn_arg.string, namelen,
&obj_method_idx);
if (obj_method == NULL)
{
SOURCING_LNUM = iptr->isn_lnum;
semsg(_(e_variable_not_found_on_object_str_str), iptr->isn_arg.string,
obj->obj_class->class_name);
return FAIL;
}
// Protected methods are not accessible outside the class
if (*obj_method->uf_name == '_'
&& !class_instance_of(current_class, obj->obj_class))
{
semsg(_(e_cannot_access_protected_method_str), obj_method->uf_name);
return FAIL;
}
// Create a partial for the member function
if (obj_method_to_partial_tv(obj, obj_method, tv) == FAIL)
return FAIL;
return OK;
}
/*
* Execute instructions in execution context "ectx".
* Return OK or FAIL;
@ -5482,6 +5557,7 @@ exec_instructions(ectx_T *ectx)
}
break;
// dict member with string key (dict['member'])
case ISN_MEMBER:
{
dict_T *dict;
@ -5526,35 +5602,51 @@ exec_instructions(ectx_T *ectx)
}
break;
// dict member with string key
// dict member with string key (dict.member)
// or can be an object
case ISN_STRINGMEMBER:
{
dict_T *dict;
dictitem_T *di;
tv = STACK_TV_BOT(-1);
if (tv->v_type != VAR_DICT || tv->vval.v_dict == NULL)
{
SOURCING_LNUM = iptr->isn_lnum;
emsg(_(e_dictionary_required));
goto on_error;
}
dict = tv->vval.v_dict;
if ((di = dict_find(dict, iptr->isn_arg.string, -1))
== NULL)
if (tv->v_type == VAR_OBJECT)
{
SOURCING_LNUM = iptr->isn_lnum;
semsg(_(e_key_not_present_in_dictionary_str),
iptr->isn_arg.string);
goto on_error;
}
// Put the dict used on the dict stack, it might be used by
// a dict function later.
if (dict_stack_save(tv) == FAIL)
goto on_fatal_error;
if (dict_stack_save(tv) == FAIL)
goto on_fatal_error;
copy_tv(&di->di_tv, tv);
ufunc_T *ufunc = (((dfunc_T *)def_functions.ga_data)
+ ectx->ec_dfunc_idx)->df_ufunc;
// Class object (not a Dict)
if (any_var_get_obj_member(ufunc->uf_class, iptr, tv) == FAIL)
goto on_error;
}
else
{
if (tv->v_type != VAR_DICT || tv->vval.v_dict == NULL)
{
SOURCING_LNUM = iptr->isn_lnum;
emsg(_(e_dictionary_required));
goto on_error;
}
dict = tv->vval.v_dict;
if ((di = dict_find(dict, iptr->isn_arg.string, -1))
== NULL)
{
SOURCING_LNUM = iptr->isn_lnum;
semsg(_(e_key_not_present_in_dictionary_str),
iptr->isn_arg.string);
goto on_error;
}
// Put the dict used on the dict stack, it might be
// used by a dict function later.
if (dict_stack_save(tv) == FAIL)
goto on_fatal_error;
copy_tv(&di->di_tv, tv);
}
}
break;