changeset 33025:1d18c7fe609f v9.0.1804

patch 9.0.1804: Vim9: no support for private object methods Commit: https://github.com/vim/vim/commit/cd7293bf6c358bb0e183582a2927fc03566d29f6 Author: Yegappan Lakshmanan <yegappan@yahoo.com> Date: Sun Aug 27 19:18:23 2023 +0200 patch 9.0.1804: Vim9: no support for private object methods Problem: Vim9: no support for private object methods Solution: Add support for private object/class methods closes: #12920 Signed-off-by: Christian Brabandt <cb@256bit.org> Co-authored-by: Yegappan Lakshmanan <yegappan@yahoo.com>
author Christian Brabandt <cb@256bit.org>
date Sun, 27 Aug 2023 19:30:05 +0200
parents e5b8fc37e882
children 4b7b89b356e8
files runtime/doc/todo.txt runtime/doc/vim9class.txt src/errors.h src/structs.h src/testdir/test_vim9_class.vim src/userfunc.c src/version.c src/vim9class.c src/vim9expr.c
diffstat 9 files changed, 691 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/todo.txt
+++ b/runtime/doc/todo.txt
@@ -129,6 +129,7 @@ Further Vim9 improvements, possibly afte
 	    or: def _Func()
     Perhaps use "private" keyword instead of "_" prefix?
   - "final" object members - can only be set in the constructor.
+  - Support export/import of classes and interfaces.
   - Cannot use class type of itself in the method (Issue #12369)
   - Cannot use an object method in a lambda  #12417
 	Define all methods before compiling them?
--- a/runtime/doc/vim9class.txt
+++ b/runtime/doc/vim9class.txt
@@ -178,6 +178,26 @@ number to the total number of lines: >
 	enddef
 
 
+Private methods ~
+If you want object methods to be accessible only from other methods of the
+same class and not used from outside the class, then you can make them
+private.  This is done by prefixing the method name with an underscore: >
+
+    class SomeClass
+	def _Foo(): number
+	  return 10
+	enddef
+	def Bar(): number
+	  return this._Foo()
+	enddef
+    endclass
+<
+Accessing a private method outside the class will result in an error (using
+the above class): >
+
+    var a = SomeClass.new()
+    a._Foo()
+<
 Simplifying the new() method ~
 
 Many constructors take values for the object members.  Thus you very often see
@@ -284,6 +304,18 @@ object members, they cannot use the "thi
 Inside the class the function can be called by name directly, outside the
 class the class name must be prefixed: `OtherThing.ClearTotalSize()`.
 
+Just like object methods the access can be made private by using an underscore
+as the first character in the method name: >
+
+    class OtherThing
+	static def _Foo()
+	    echo "Foo"
+	enddef
+	def Bar()
+	    OtherThing._Foo()
+	enddef
+    endclass
+
 ==============================================================================
 
 4.  Using an abstract class			*Vim9-abstract-class*
--- a/src/errors.h
+++ b/src/errors.h
@@ -3484,6 +3484,11 @@ EXTERN char e_warning_pointer_block_corr
 	INIT(= N_("E1364: Warning: Pointer block corrupted"));
 EXTERN char e_cannot_use_a_return_type_with_new[]
 	INIT(= N_("E1365: Cannot use a return type with the \"new\" function"));
+EXTERN char e_cannot_access_private_method_str[]
+	INIT(= N_("E1366: Cannot access private method: %s"));
+
+EXTERN char e_interface_str_and_class_str_function_access_not_same[]
+	INIT(= N_("E1367: Access type of class method %s differs from interface method %s"));
 EXTERN char e_cannot_mix_positional_and_non_positional_str[]
 	INIT(= N_("E1400: Cannot mix positional and non-positional arguments: %s"));
 EXTERN char e_fmt_arg_nr_unused_str[]
@@ -3501,4 +3506,4 @@ EXTERN char e_member_str_type_mismatch_e
 EXTERN char e_method_str_type_mismatch_expected_str_but_got_str[]
 	INIT(= N_("E1407: Member \"%s\": type mismatch, expected %s but got %s"));
 
-// E1366 - E1399 unused
+// E1368 - E1399 unused
--- a/src/structs.h
+++ b/src/structs.h
@@ -1489,8 +1489,8 @@ typedef struct {
 #define TTFLAG_SUPER	    0x40    // object from "super".
 
 typedef enum {
-    VIM_ACCESS_PRIVATE,	// read/write only inside th class
-    VIM_ACCESS_READ,	// read everywhere, write only inside th class
+    VIM_ACCESS_PRIVATE,	// read/write only inside the class
+    VIM_ACCESS_READ,	// read everywhere, write only inside the class
     VIM_ACCESS_ALL	// read/write everywhere
 } omacc_T;
 
@@ -1790,6 +1790,7 @@ struct ufunc_S
 
     class_T	*uf_class;	// for object method and constructor; does not
 				// count for class_refcount
+    int		uf_private;	// TRUE if class or object private method
 
     garray_T	uf_args;	// arguments, including optional arguments
     garray_T	uf_def_args;	// default argument expressions
--- a/src/testdir/test_vim9_class.vim
+++ b/src/testdir/test_vim9_class.vim
@@ -2801,4 +2801,594 @@ def Test_object_lockvar()
   v9.CheckScriptSuccess(lines)
 enddef
 
+" Test for a private object method
+def Test_private_object_method()
+  # Try calling a private method using an object (at the script level)
+  var lines =<< trim END
+    vim9script
+
+    class A
+      def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    var a = A.new()
+    a._Foo()
+  END
+  v9.CheckScriptFailure(lines, 'E1366: Cannot access private method: _Foo()')
+
+  # Try calling a private method using an object (from a def function)
+  lines =<< trim END
+    vim9script
+
+    class A
+      def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    def T()
+      var a = A.new()
+      a._Foo()
+    enddef
+    T()
+  END
+  v9.CheckScriptFailure(lines, 'E1366: Cannot access private method: _Foo()')
+
+  # Use a private method from another object method (in script context)
+  lines =<< trim END
+    vim9script
+
+    class A
+      def _Foo(): number
+        return 1234
+      enddef
+      def Bar(): number
+        return this._Foo()
+      enddef
+    endclass
+    var a = A.new()
+    assert_equal(1234, a.Bar())
+  END
+  v9.CheckScriptSuccess(lines)
+
+  # Use a private method from another object method (def function context)
+  lines =<< trim END
+    vim9script
+
+    class A
+      def _Foo(): number
+        return 1234
+      enddef
+      def Bar(): number
+        return this._Foo()
+      enddef
+    endclass
+    def T()
+      var a = A.new()
+      assert_equal(1234, a.Bar())
+    enddef
+    T()
+  END
+  v9.CheckScriptSuccess(lines)
+
+  # Try calling a private method without the "this" prefix
+  lines =<< trim END
+    vim9script
+
+    class A
+      def _Foo(): number
+        return 1234
+      enddef
+      def Bar(): number
+        return _Foo()
+      enddef
+    endclass
+    var a = A.new()
+    a.Bar()
+  END
+  v9.CheckScriptFailure(lines, 'E117: Unknown function: _Foo')
+
+  # Try calling a private method using the class name
+  lines =<< trim END
+    vim9script
+
+    class A
+      def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    A._Foo()
+  END
+  v9.CheckScriptFailure(lines, 'E1325: Method not found on class "A": _Foo()')
+
+  # Try to use "public" keyword when defining a private method
+  lines =<< trim END
+    vim9script
+
+    class A
+      public def _Foo()
+      enddef
+    endclass
+    var a = A.new()
+    a._Foo()
+  END
+  v9.CheckScriptFailure(lines, 'E1331: Public must be followed by "this" or "static"')
+
+  # Define two private methods with the same name
+  lines =<< trim END
+    vim9script
+
+    class A
+      def _Foo()
+      enddef
+      def _Foo()
+      enddef
+    endclass
+    var a = A.new()
+  END
+  v9.CheckScriptFailure(lines, 'E1355: Duplicate function: _Foo')
+
+  # Define a private method and a object method with the same name
+  lines =<< trim END
+    vim9script
+
+    class A
+      def _Foo()
+      enddef
+      def Foo()
+      enddef
+    endclass
+    var a = A.new()
+  END
+  v9.CheckScriptFailure(lines, 'E1355: Duplicate function: Foo')
+
+  # Define an object method and a private method with the same name
+  lines =<< trim END
+    vim9script
+
+    class A
+      def Foo()
+      enddef
+      def _Foo()
+      enddef
+    endclass
+    var a = A.new()
+  END
+  v9.CheckScriptFailure(lines, 'E1355: Duplicate function: _Foo')
+
+  # Call a public method and a private method from a private method
+  lines =<< trim END
+    vim9script
+
+    class A
+      def Foo(): number
+        return 100
+      enddef
+      def _Bar(): number
+        return 200
+      enddef
+      def _Baz()
+        assert_equal(100, this.Foo())
+        assert_equal(200, this._Bar())
+      enddef
+      def T()
+        this._Baz()
+      enddef
+    endclass
+    var a = A.new()
+    a.T()
+  END
+  v9.CheckScriptSuccess(lines)
+
+  # Try calling a private method from another class
+  lines =<< trim END
+    vim9script
+
+    class A
+      def _Foo(): number
+        return 100
+      enddef
+    endclass
+    class B
+      def Foo(): number
+        var a = A.new()
+        a._Foo()
+      enddef
+    endclass
+    var b = B.new()
+    b.Foo()
+  END
+  v9.CheckScriptFailure(lines, 'E1366: Cannot access private method: _Foo()')
+
+  # Call a private object method from a child class object method
+  lines =<< trim END
+    vim9script
+    class A
+      def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    class B extends A
+      def Bar()
+      enddef
+    endclass
+    class C extends B
+      def Baz(): number
+        return this._Foo()
+      enddef
+    endclass
+    var c = C.new()
+    assert_equal(1234, c.Baz())
+  END
+  v9.CheckScriptSuccess(lines)
+
+  # Call a private object method from a child class object
+  lines =<< trim END
+    vim9script
+    class A
+      def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    class B extends A
+      def Bar()
+      enddef
+    endclass
+    class C extends B
+      def Baz(): number
+      enddef
+    endclass
+    var c = C.new()
+    assert_equal(1234, c._Foo())
+  END
+  v9.CheckScriptFailure(lines, 'E1366: Cannot access private method: _Foo()')
+
+  # Using "_" prefix in a method name should fail outside of a class
+  lines =<< trim END
+    vim9script
+    def _Foo(): number
+      return 1234
+    enddef
+    var a = _Foo()
+  END
+  v9.CheckScriptFailure(lines, 'E1267: Function name must start with a capital: _Foo(): number')
+enddef
+
+" Test for an private class method
+def Test_private_class_method()
+  # Try calling a class private method (at the script level)
+  var lines =<< trim END
+    vim9script
+
+    class A
+      static def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    A._Foo()
+  END
+  v9.CheckScriptFailure(lines, 'E1366: Cannot access private method: _Foo()')
+
+  # Try calling a class private method (from a def function)
+  lines =<< trim END
+    vim9script
+
+    class A
+      static def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    def T()
+      A._Foo()
+    enddef
+    T()
+  END
+  v9.CheckScriptFailure(lines, 'E1366: Cannot access private method: _Foo()')
+
+  # Try calling a class private method using an object (at the script level)
+  lines =<< trim END
+    vim9script
+
+    class A
+      static def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    var a = A.new()
+    a._Foo()
+  END
+  v9.CheckScriptFailure(lines, 'E1325: Method not found on class "A": _Foo()')
+
+  # Try calling a class private method using an object (from a def function)
+  lines =<< trim END
+    vim9script
+
+    class A
+      static def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    def T()
+      var a = A.new()
+      a._Foo()
+    enddef
+    T()
+  END
+  v9.CheckScriptFailure(lines, 'E1325: Method not found on class "A": _Foo()')
+
+  # Use a class private method from an object method
+  lines =<< trim END
+    vim9script
+
+    class A
+      static def _Foo(): number
+        return 1234
+      enddef
+      def Bar()
+        assert_equal(1234, A._Foo())
+      enddef
+    endclass
+    var a = A.new()
+    a.Bar()
+  END
+  v9.CheckScriptSuccess(lines)
+
+  # Use a class private method from another class private method
+  lines =<< trim END
+    vim9script
+
+    class A
+      static def _Foo1(): number
+        return 1234
+      enddef
+      static def _Foo2()
+        assert_equal(1234, A._Foo1())
+      enddef
+      def Bar()
+        A._Foo2()
+      enddef
+    endclass
+    var a = A.new()
+    a.Bar()
+  END
+  v9.CheckScriptSuccess(lines)
+
+  # Declare a class method and a class private method with the same name
+  lines =<< trim END
+    vim9script
+
+    class A
+      static def _Foo()
+      enddef
+      static def Foo()
+      enddef
+    endclass
+    var a = A.new()
+  END
+  v9.CheckScriptFailure(lines, 'E1355: Duplicate function: Foo')
+
+  # Try calling a class private method from another class
+  lines =<< trim END
+    vim9script
+
+    class A
+      static def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    class B
+      def Foo(): number
+        return A._Foo()
+      enddef
+    endclass
+    var b = B.new()
+    assert_equal(1234, b.Foo())
+  END
+  v9.CheckScriptFailure(lines, 'E1366: Cannot access private method: _Foo()')
+
+  # Call a private class method from a child class object method
+  lines =<< trim END
+    vim9script
+    class A
+      static def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    class B extends A
+      def Bar()
+      enddef
+    endclass
+    class C extends B
+      def Baz(): number
+        return A._Foo()
+      enddef
+    endclass
+    var c = C.new()
+    assert_equal(1234, c.Baz())
+  END
+  v9.CheckScriptSuccess(lines)
+
+  # Call a private class method from a child class private class method
+  lines =<< trim END
+    vim9script
+    class A
+      static def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    class B extends A
+      def Bar()
+      enddef
+    endclass
+    class C extends B
+      static def Baz(): number
+        return A._Foo()
+      enddef
+    endclass
+    assert_equal(1234, C.Baz())
+  END
+  v9.CheckScriptSuccess(lines)
+
+  # Call a private class method from a child class object
+  lines =<< trim END
+    vim9script
+    class A
+      static def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    class B extends A
+      def Bar()
+      enddef
+    endclass
+    class C extends B
+      def Baz(): number
+      enddef
+    endclass
+    var c = C.new()
+    assert_equal(1234, C._Foo())
+  END
+  v9.CheckScriptFailure(lines, 'E1366: Cannot access private method: _Foo()')
+enddef
+
+" Test for an interface private object_method
+def Test_interface_private_object_method()
+  # Implement an interface private method and use it from a public method
+  var lines =<< trim END
+    vim9script
+    interface Intf
+      def _Foo(): number
+    endinterface
+    class A implements Intf
+      def _Foo(): number
+        return 1234
+      enddef
+      def Bar(): number
+        return this._Foo()
+      enddef
+    endclass
+    var a = A.new()
+    assert_equal(1234, a.Bar())
+  END
+  v9.CheckScriptSuccess(lines)
+
+  # Call an interface private class method (script context)
+  lines =<< trim END
+    vim9script
+    interface Intf
+      def _Foo(): number
+    endinterface
+    class A implements Intf
+      def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    var a = A.new()
+    assert_equal(1234, a._Foo())
+  END
+  v9.CheckScriptFailure(lines, 'E1366: Cannot access private method: _Foo()')
+
+  # Call an interface private class method (def context)
+  lines =<< trim END
+    vim9script
+    interface Intf
+      def _Foo(): number
+    endinterface
+    class A implements Intf
+      def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    def T()
+      var a = A.new()
+      assert_equal(1234, a._Foo())
+    enddef
+    T()
+  END
+  v9.CheckScriptFailure(lines, 'E1366: Cannot access private method: _Foo()')
+
+  # Implement an interface private object method as a private class method
+  lines =<< trim END
+    vim9script
+    interface Intf
+      def _Foo(): number
+    endinterface
+    class A implements Intf
+      static def _Foo(): number
+        return 1234
+      enddef
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1349: Function "_Foo" of interface "Intf" not implemented')
+enddef
+
+" Test for an interface private class method
+def Test_interface_private_class_method()
+  # Implement an interface private class method and use it from a public method
+  var lines =<< trim END
+    vim9script
+    interface Intf
+      static def _Foo(): number
+    endinterface
+    class A implements Intf
+      static def _Foo(): number
+        return 1234
+      enddef
+      def Bar(): number
+        return A._Foo()
+      enddef
+    endclass
+    var a = A.new()
+    assert_equal(1234, a.Bar())
+  END
+  v9.CheckScriptSuccess(lines)
+
+  # Call an interface private class method (script context)
+  lines =<< trim END
+    vim9script
+    interface Intf
+      static def _Foo(): number
+    endinterface
+    class A implements Intf
+      static def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    assert_equal(1234, A._Foo())
+  END
+  v9.CheckScriptFailure(lines, 'E1366: Cannot access private method: _Foo())')
+
+  # Call an interface private class method (def context)
+  lines =<< trim END
+    vim9script
+    interface Intf
+      static def _Foo(): number
+    endinterface
+    class A implements Intf
+      static def _Foo(): number
+        return 1234
+      enddef
+    endclass
+    def T()
+      assert_equal(1234, A._Foo())
+    enddef
+    T()
+  END
+  v9.CheckScriptFailure(lines, 'E1366: Cannot access private method: _Foo())')
+
+  # Implement an interface private class method as a private object method
+  lines =<< trim END
+    vim9script
+    interface Intf
+      static def _Foo(): number
+    endinterface
+    class A implements Intf
+      def _Foo(): number
+        return 1234
+      enddef
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1349: Function "_Foo" of interface "Intf" not implemented')
+enddef
+
 " vim: ts=8 sw=2 sts=2 expandtab tw=80 fdm=marker
--- a/src/userfunc.c
+++ b/src/userfunc.c
@@ -4369,10 +4369,14 @@ trans_function_name_ext(
 		lead += (int)STRLEN(sid_buf);
 	}
     }
+    // The function name must start with an upper case letter (unless it is a
+    // Vim9 class new() function or a Vim9 class private method)
     else if (!(flags & TFN_INT)
 	    && (builtin_function(lv.ll_name, len)
 				   || (vim9script && *lv.ll_name == '_'))
-	    && !((flags & TFN_IN_CLASS) && STRNCMP(lv.ll_name, "new", 3) == 0))
+	    && !((flags & TFN_IN_CLASS)
+		&& (STRNCMP(lv.ll_name, "new", 3) == 0
+		    || (*lv.ll_name == '_'))))
     {
 	semsg(_(vim9script ? e_function_name_must_start_with_capital_str
 			   : e_function_name_must_start_with_capital_or_s_str),
--- a/src/version.c
+++ b/src/version.c
@@ -700,6 +700,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1804,
+/**/
     1803,
 /**/
     1802,
--- a/src/vim9class.c
+++ b/src/vim9class.c
@@ -357,6 +357,13 @@ validate_interface_methods(
 		    if (check_type_maybe(if_fp[if_i]->uf_func_type,
 				cl_fp[cl_i]->uf_func_type, TRUE, where) != OK)
 			success = FALSE;
+		    // Ensure the public/private access level is matching.
+		    if (if_fp[if_i]->uf_private != cl_fp[cl_i]->uf_private)
+		    {
+			semsg(_(e_interface_str_and_class_str_function_access_not_same),
+				cl_name, if_name);
+			success = FALSE;
+		    }
 		    break;
 		}
 	    }
@@ -1150,7 +1157,9 @@ early_ret:
 		for (int i = 0; i < fgap->ga_len; ++i)
 		{
 		    char_u *n = ((ufunc_T **)fgap->ga_data)[i]->uf_name;
-		    if (STRCMP(name, n) == 0)
+		    char_u *pstr = *name == '_' ? name + 1 : name;
+		    char_u *qstr = *n == '_' ? n + 1 : n;
+		    if (STRCMP(pstr, qstr) == 0)
 		    {
 			semsg(_(e_duplicate_function_str), name);
 			break;
@@ -1162,6 +1171,11 @@ early_ret:
 		    if (is_new)
 			uf->uf_flags |= FC_NEW;
 
+		    // If the method name starts with '_', then it a private
+		    // method.
+		    if (*name == '_')
+			uf->uf_private = TRUE;
+
 		    ((ufunc_T **)fgap->ga_data)[fgap->ga_len] = uf;
 		    ++fgap->ga_len;
 		}
@@ -1523,6 +1537,13 @@ class_object_index(
 		typval_T    argvars[MAX_FUNC_ARGS + 1];
 		int	    argcount = 0;
 
+		if (fp->uf_private)
+		{
+		    // Cannot access a private method outside of a class
+		    semsg(_(e_cannot_access_private_method_str), name);
+		    return FAIL;
+		}
+
 		char_u *argp = name_end;
 		int ret = get_func_arguments(&argp, evalarg, 0,
 							   argvars, &argcount);
--- a/src/vim9expr.c
+++ b/src/vim9expr.c
@@ -252,6 +252,30 @@ compile_member(int is_slice, int *keepin
 }
 
 /*
+ * Returns TRUE if the current function is inside the class "cl" or one of the
+ * parent classes.
+ */
+    static int
+inside_class_hierarchy(cctx_T *cctx_arg, class_T *cl)
+{
+    for (cctx_T *cctx = cctx_arg; cctx != NULL; cctx = cctx->ctx_outer)
+    {
+	if (cctx->ctx_ufunc != NULL && cctx->ctx_ufunc->uf_class != NULL)
+	{
+	    class_T	*clp = cctx->ctx_ufunc->uf_class;
+	    while (clp != NULL)
+	    {
+		if (clp == cl)
+		    return TRUE;
+		clp = clp->class_extends;
+	    }
+	}
+    }
+
+    return FALSE;
+}
+
+/*
  * Compile ".member" coming after an object or class.
  */
     static int
@@ -348,6 +372,12 @@ compile_class_object_index(cctx_T *cctx,
 	    return FAIL;
 	}
 
+	if (ufunc->uf_private && !inside_class_hierarchy(cctx, cl))
+	{
+	    semsg(_(e_cannot_access_private_method_str), name);
+	    return FAIL;
+	}
+
 	// Compile the arguments and call the class function or object method.
 	// The object method will know that the object is on the stack, just
 	// before the arguments.