# HG changeset patch # User Christian Brabandt # Date 1693157405 -7200 # Node ID 1d18c7fe609f0757c7c93bb6510ae1a985906560 # Parent e5b8fc37e882c936632d93072700b2891be0b45a patch 9.0.1804: Vim9: no support for private object methods Commit: https://github.com/vim/vim/commit/cd7293bf6c358bb0e183582a2927fc03566d29f6 Author: Yegappan Lakshmanan 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 Co-authored-by: Yegappan Lakshmanan diff --git a/runtime/doc/todo.txt b/runtime/doc/todo.txt --- 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? diff --git a/runtime/doc/vim9class.txt b/runtime/doc/vim9class.txt --- 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* diff --git a/src/errors.h b/src/errors.h --- 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 diff --git a/src/structs.h b/src/structs.h --- 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 diff --git a/src/testdir/test_vim9_class.vim b/src/testdir/test_vim9_class.vim --- 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 diff --git a/src/userfunc.c b/src/userfunc.c --- 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), diff --git a/src/version.c b/src/version.c --- 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, diff --git a/src/vim9class.c b/src/vim9class.c --- 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); diff --git a/src/vim9expr.c b/src/vim9expr.c --- 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.