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.

Raspunde prin e-mail lui