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>
diff --git a/src/proto/vim9class.pro b/src/proto/vim9class.pro
index 7d11523..d59caa0 100644
--- a/src/proto/vim9class.pro
+++ b/src/proto/vim9class.pro
@@ -9,6 +9,8 @@
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);
diff --git a/src/testdir/test_vim9_class.vim b/src/testdir/test_vim9_class.vim
index 8791a52..4ce9fcd 100644
--- a/src/testdir/test_vim9_class.vim
+++ b/src/testdir/test_vim9_class.vim
@@ -784,7 +784,7 @@
vim9script
class Inner
- var value: number = 0
+ public var value: number = 0
endclass
class Outer
@@ -11213,4 +11213,339 @@
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
diff --git a/src/version.c b/src/version.c
index 7906b07..636dfaa 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
+ 850,
+/**/
849,
/**/
848,
diff --git a/src/vim9class.c b/src/vim9class.c
index 87b4d45..d0ddcb8 100644
--- a/src/vim9class.c
+++ b/src/vim9class.c
@@ -2777,12 +2777,13 @@
* "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 @@
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 @@
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;
}
@@ -2917,6 +2919,33 @@
}
/*
+ * 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
* - class function: SomeClass.SomeMethod()
@@ -2978,7 +3007,7 @@
// 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 @@
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;
}
diff --git a/src/vim9execute.c b/src/vim9execute.c
index f523e27..da03d5e 100644
--- a/src/vim9execute.c
+++ b/src/vim9execute.c
@@ -2268,15 +2268,23 @@
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
{
@@ -3120,6 +3128,73 @@
}
/*
+ * 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 @@
}
break;
+ // dict member with string key (dict['member'])
case ISN_MEMBER:
{
dict_T *dict;
@@ -5526,35 +5602,51 @@
}
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;