patch 9.1.0850: Vim9: cannot access nested object inside objects Commit: https://github.com/vim/vim/commit/56d45f1b6658ca64857b4cb22f18a18eeefa0f1d Author: Yegappan Lakshmanan <yegap...@yahoo.com> Date: Mon Nov 11 19:58:55 2024 +0100
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 <yegap...@yahoo.com> Signed-off-by: Christian Brabandt <c...@256bit.org> diff --git a/runtime/doc/tags b/runtime/doc/tags index 3039737f5..d38c8958e 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -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()* diff --git a/runtime/doc/vim9class.txt b/runtime/doc/vim9class.txt index ef96aa907..a06c2c6e0 100644 --- a/runtime/doc/vim9class.txt +++ b/runtime/doc/vim9class.txt @@ -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 diff --git a/src/proto/vim9class.pro b/src/proto/vim9class.pro index 7d11523ce..d59caa067 100644 --- a/src/proto/vim9class.pro +++ b/src/proto/vim9class.pro @@ -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); diff --git a/src/testdir/test_vim9_class.vim b/src/testdir/test_vim9_class.vim index 8791a5218..4ce9fcdc4 100644 --- a/src/testdir/test_vim9_class.vim +++ b/src/testdir/test_vim9_class.vim @@ -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 diff --git a/src/version.c b/src/version.c index 7906b07e0..636dfaa12 100644 --- a/src/version.c +++ b/src/version.c @@ -704,6 +704,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 850, /**/ 849, /**/ diff --git a/src/vim9class.c b/src/vim9class.c index 87b4d45ea..d0ddcb820 100644 --- a/src/vim9class.c +++ b/src/vim9class.c @@ -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; } diff --git a/src/vim9execute.c b/src/vim9execute.c index f523e27b8..da03d5e7d 100644 --- a/src/vim9execute.c +++ b/src/vim9execute.c @@ -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; + if (dict_stack_save(tv) == FAIL) + goto on_fatal_error; + + 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; } - // 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; + 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; - copy_tv(&di->di_tv, tv); + 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; -- -- You received this message from the "vim_dev" maillist. Do not top-post! Type your reply below the text you are replying to. For more information, visit http://www.vim.org/maillist.php --- You received this message because you are subscribed to the Google Groups "vim_dev" group. To unsubscribe from this group and stop receiving emails from it, send an email to vim_dev+unsubscr...@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/vim_dev/E1tAZsS-00ErtO-Cg%40256bit.org.