patch 9.0.1737: Calling a base class method through an extended class fails

Problem: Calling a base class method through an extended class fails
Solution: Create lookup table for member index in the interface to
          to the member class implementing the interface

Create additional tests for Vim9 classes.  Fix unconvered memory leaks
and crashes found by the new tests.

closes: #12848
closes: #12089

Signed-off-by: Christian Brabandt <cb@256bit.org>
Co-authored-by: Yegappan Lakshmanan <yegappan@yahoo.com>author
diff --git a/src/testdir/test_vim9_class.vim b/src/testdir/test_vim9_class.vim
index cd67b54..684bf6d 100644
--- a/src/testdir/test_vim9_class.vim
+++ b/src/testdir/test_vim9_class.vim
@@ -135,6 +135,16 @@
   END
   v9.CheckScriptFailure(lines, 'E1069:')
 
+  # Test for unsupported comment specifier
+  lines =<< trim END
+    vim9script
+    class Something
+      # comment
+      #{
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1170:')
+
   lines =<< trim END
       vim9script
 
@@ -162,6 +172,44 @@
       assert_equal('object<TextPosition>', typename(pos))
   END
   v9.CheckScriptSuccess(lines)
+
+  # When referencing object methods, space cannot be used after a "."
+  lines =<< trim END
+    vim9script
+    class A
+      def Foo(): number
+        return 10
+      enddef
+    endclass
+    var a = A.new()
+    var v = a. Foo()
+  END
+  v9.CheckScriptFailure(lines, 'E1202:')
+
+  # Using an object without specifying a method or a member variable
+  lines =<< trim END
+    vim9script
+    class A
+      def Foo(): number
+        return 10
+      enddef
+    endclass
+    var a = A.new()
+    var v = a.
+  END
+  v9.CheckScriptFailure(lines, 'E15:')
+
+  # Error when parsing the arguments of an object method.
+  lines =<< trim END
+    vim9script
+    class A
+      def Foo()
+      enddef
+    endclass
+    var a = A.new()
+    var v = a.Foo(,)
+  END
+  v9.CheckScriptFailure(lines, 'E15:')
 enddef
 
 def Test_class_defined_twice()
@@ -628,6 +676,15 @@
       endclass
   END
   v9.CheckScriptFailure(lines, 'E1330:')
+
+  # Test for initializing an object member with an unknown variable/type
+  lines =<< trim END
+    vim9script
+    class A
+       this.value = init_val
+    endclass
+  END
+  v9.CheckScriptFailureList(lines, ['E121:', 'E1329:'])
 enddef
 
 def Test_class_object_member_access()
@@ -658,6 +715,15 @@
   END
   v9.CheckScriptSuccess(lines)
 
+  # Test for a public member variable name beginning with an underscore
+  lines =<< trim END
+    vim9script
+    class A
+      public this._val = 10
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1332:')
+
   lines =<< trim END
       vim9script
 
@@ -735,6 +801,33 @@
             .x
   END
   v9.CheckScriptSuccess(lines)
+
+  # Test for "public" cannot be abbreviated
+  lines =<< trim END
+    vim9script
+    class Something
+      pub this.val = 1
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1065:')
+
+  # Test for "public" keyword must be followed by "this" or "static".
+  lines =<< trim END
+    vim9script
+    class Something
+      public val = 1
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1331:')
+
+  # Test for "static" cannot be abbreviated
+  lines =<< trim END
+    vim9script
+    class Something
+      stat this.val = 1
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1065:')
 enddef
 
 def Test_class_object_compare()
@@ -1002,6 +1095,42 @@
       s.Method(7)
   END
   v9.CheckScriptFailure(lines, 'E1341: Variable already declared in the class: count')
+
+  # Test for using an invalid type for a member variable
+  lines =<< trim END
+    vim9script
+    class A
+      this.val: xxx
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1010:')
+
+  # Test for no space before or after the '=' when initializing a member
+  # variable
+  lines =<< trim END
+    vim9script
+    class A
+      this.val: number= 10
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1004:')
+  lines =<< trim END
+    vim9script
+    class A
+      this.val: number =10
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1004:')
+
+  # Access a non-existing member
+  lines =<< trim END
+    vim9script
+    class A
+    endclass
+    var a = A.new()
+    var v = a.bar
+  END
+  v9.CheckScriptFailure(lines, 'E1326:')
 enddef
 
 func Test_class_garbagecollect()
@@ -1072,6 +1201,18 @@
       assert_equal(2, Value.GetCount())
   END
   v9.CheckScriptSuccess(lines)
+
+  # Test for cleaning up after a class definition failure when using class
+  # functions.
+  lines =<< trim END
+    vim9script
+    class A
+      static def Foo()
+      enddef
+      aaa
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1318:')
 enddef
 
 def Test_class_defcompile()
@@ -1100,6 +1241,40 @@
       defcompile C.Fc
   END
   v9.CheckScriptFailure(lines, 'E1012: Type mismatch; expected number but got string')
+
+  # Trying to compile a function using a non-existing class variable
+  lines =<< trim END
+    vim9script
+    defcompile x.Foo()
+  END
+  v9.CheckScriptFailure(lines, 'E475:')
+
+  # Trying to compile a function using a variable which is not a class
+  lines =<< trim END
+    vim9script
+    var x: number
+    defcompile x.Foo()
+  END
+  v9.CheckScriptFailure(lines, 'E475:')
+
+  # Trying to compile a function without specifying the name
+  lines =<< trim END
+    vim9script
+    class A
+    endclass
+    defcompile A.
+  END
+  v9.CheckScriptFailure(lines, 'E475:')
+
+  # Trying to compile a non-existing class object member function
+  lines =<< trim END
+    vim9script
+    class A
+    endclass
+    var a = A.new()
+    defcompile a.Foo()
+  END
+  v9.CheckScriptFailureList(lines, ['E1334:', 'E475:'])
 enddef
 
 def Test_class_object_to_string()
@@ -1345,6 +1520,62 @@
       Test()
   END
   v9.CheckScriptSuccess(lines)
+
+  # Interface name after "extends" doesn't end in a space or NUL character
+  lines =<< trim END
+    vim9script
+    interface A
+    endinterface
+    class B extends A"
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1315:')
+
+  # Trailing characters after a class name
+  lines =<< trim END
+    vim9script
+    class A bbb
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E488:')
+
+  # using "implements" with a non-existing class
+  lines =<< trim END
+    vim9script
+    class A implements B
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1346:')
+
+  # using "implements" with a regular class
+  lines =<< trim END
+    vim9script
+    class A
+    endclass
+    class B implements A
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1347:')
+
+  # using "implements" with a variable
+  lines =<< trim END
+    vim9script
+    var T: number = 10
+    class A implements T
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1347:')
+
+  # all the class methods in an "interface" should be implemented
+  lines =<< trim END
+    vim9script
+    interface A
+      static def Foo()
+    endinterface
+    class B implements A
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1349:')
 enddef
 
 def Test_call_interface_method()
@@ -1717,6 +1948,16 @@
       assert_equal("object of Success {success: true, value: 'asdf'}", string(v))
   END
   v9.CheckScriptSuccess(lines)
+
+  # class name after "extends" doesn't end in a space or NUL character
+  lines =<< trim END
+    vim9script
+    class A
+    endclass
+    class B extends A"
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1315:')
 enddef
 
 def Test_using_base_class()
@@ -1841,6 +2082,16 @@
       endclass
   END
   v9.CheckScriptFailure(lines, 'E1316:')
+
+  # Abstract class cannot have a "new" function
+  lines =<< trim END
+    vim9script
+    abstract class Base
+      def new()
+      enddef
+    endclass
+  END
+  v9.CheckScriptFailure(lines, 'E1359:')
 enddef
 
 def Test_closure_in_class()
@@ -2063,4 +2314,127 @@
   v9.CheckScriptSuccess(lines)
 enddef
 
+" Test for calling methods from three levels of classes
+def Test_multi_level_method_call()
+  var lines =<< trim END
+    vim9script
+
+    var A_func1: number = 0
+    var A_func2: number = 0
+    var A_func3: number = 0
+    var B_func2: number = 0
+    var B_func3: number = 0
+    var C_func3: number = 0
+
+    class A
+      def Func1()
+        A_func1 += 1
+      enddef
+
+      def Func2()
+        A_func2 += 1
+      enddef
+
+      def Func3()
+        A_func3 += 1
+      enddef
+    endclass
+
+    class B extends A
+      def Func2()
+        B_func2 += 1
+      enddef
+
+      def Func3()
+        B_func3 += 1
+      enddef
+    endclass
+
+    class C extends B
+      def Func3()
+        C_func3 += 1
+      enddef
+    endclass
+
+    def A_CallFuncs(a: A)
+      a.Func1()
+      a.Func2()
+      a.Func3()
+    enddef
+
+    def B_CallFuncs(b: B)
+      b.Func1()
+      b.Func2()
+      b.Func3()
+    enddef
+
+    def C_CallFuncs(c: C)
+      c.Func1()
+      c.Func2()
+      c.Func3()
+    enddef
+
+    var cobj = C.new()
+    A_CallFuncs(cobj)
+    B_CallFuncs(cobj)
+    C_CallFuncs(cobj)
+    assert_equal(3, A_func1)
+    assert_equal(0, A_func2)
+    assert_equal(0, A_func3)
+    assert_equal(3, B_func2)
+    assert_equal(0, B_func3)
+    assert_equal(3, C_func3)
+  END
+  v9.CheckScriptSuccess(lines)
+enddef
+
+" Test for using members from three levels of classes
+def Test_multi_level_member_access()
+  var lines =<< trim END
+    vim9script
+
+    class A
+      this.val1: number = 0
+      this.val2: number = 0
+      this.val3: number = 0
+    endclass
+
+    class B extends A
+      this.val2: number = 0
+      this.val3: number = 0
+    endclass
+
+    class C extends B
+      this.val3: number = 0
+    endclass
+
+    def A_members(a: A)
+      a.val1 += 1
+      a.val2 += 1
+      a.val3 += 1
+    enddef
+
+    def B_members(b: B)
+      b.val1 += 1
+      b.val2 += 1
+      b.val3 += 1
+    enddef
+
+    def C_members(c: C)
+      c.val1 += 1
+      c.val2 += 1
+      c.val3 += 1
+    enddef
+
+    var cobj = C.new()
+    A_members(cobj)
+    B_members(cobj)
+    C_members(cobj)
+    assert_equal(3, cobj.val1)
+    assert_equal(3, cobj.val2)
+    assert_equal(3, cobj.val3)
+  END
+  v9.CheckScriptSuccess(lines)
+enddef
+
 " vim: ts=8 sw=2 sts=2 expandtab tw=80 fdm=marker
diff --git a/src/version.c b/src/version.c
index f120730..325a46e 100644
--- a/src/version.c
+++ b/src/version.c
@@ -696,6 +696,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1737,
+/**/
     1736,
 /**/
     1735,
diff --git a/src/vim9class.c b/src/vim9class.c
index b04fb0f..e310dec 100644
--- a/src/vim9class.c
+++ b/src/vim9class.c
@@ -231,6 +231,117 @@
 }
 
 /*
+ * Update the interface class lookup table for the member index on the
+ * interface to the member index in the class implementing the interface.
+ * And a lookup table for the object method index on the interface
+ * to the object method index in the class implementing the interface.
+ * This is also used for updating the lookup table for the extended class
+ * hierarchy.
+ */
+    static int
+update_member_method_lookup_table(
+	class_T		*ifcl,
+	class_T		*cl,
+	garray_T	*objmethods,
+	int		pobj_method_offset,
+	int		is_interface)
+{
+    if (ifcl == NULL)
+	return OK;
+
+    // Table for members.
+    itf2class_T *if2cl = alloc_clear(sizeof(itf2class_T)
+	    + ifcl->class_obj_member_count * sizeof(int));
+    if (if2cl == NULL)
+	return FAIL;
+    if2cl->i2c_next = ifcl->class_itf2class;
+    ifcl->class_itf2class = if2cl;
+    if2cl->i2c_class = cl;
+    if2cl->i2c_is_method = FALSE;
+
+    for (int if_i = 0; if_i < ifcl->class_obj_member_count; ++if_i)
+	for (int cl_i = 0; cl_i < cl->class_obj_member_count; ++cl_i)
+	{
+	    if (STRCMP(ifcl->class_obj_members[if_i].ocm_name,
+			cl->class_obj_members[cl_i].ocm_name) == 0)
+	    {
+		int *table = (int *)(if2cl + 1);
+		table[if_i] = cl_i;
+		break;
+	    }
+	}
+
+    // Table for methods.
+    if2cl = alloc_clear(sizeof(itf2class_T)
+	    + ifcl->class_obj_method_count * sizeof(int));
+    if (if2cl == NULL)
+	return FAIL;
+    if2cl->i2c_next = ifcl->class_itf2class;
+    ifcl->class_itf2class = if2cl;
+    if2cl->i2c_class = cl;
+    if2cl->i2c_is_method = TRUE;
+
+    for (int if_i = 0; if_i < ifcl->class_obj_method_count; ++if_i)
+    {
+	int done = FALSE;
+	for (int cl_i = 0; cl_i < objmethods->ga_len; ++cl_i)
+	{
+	    if (STRCMP(ifcl->class_obj_methods[if_i]->uf_name,
+			((ufunc_T **)objmethods->ga_data)[cl_i]->uf_name)
+		    == 0)
+	    {
+		int *table = (int *)(if2cl + 1);
+		table[if_i] = cl_i;
+		done = TRUE;
+		break;
+	    }
+	}
+
+	// extended class object method is not overridden by the child class.
+	// Keep the method declared in one of the parent classes in the
+	// lineage.
+	if (!done && !is_interface)
+	{
+	    // If "ifcl" is not the immediate parent of "cl", then search in
+	    // the intermediate parent classes.
+	    if (cl->class_extends != ifcl)
+	    {
+		class_T	    *parent = cl->class_extends;
+		int	    method_offset = objmethods->ga_len;
+
+		while (!done && parent != NULL && parent != ifcl)
+		{
+
+		    for (int cl_i = 0;
+			    cl_i < parent->class_obj_method_count_child; ++cl_i)
+		    {
+			if (STRCMP(ifcl->class_obj_methods[if_i]->uf_name,
+				    parent->class_obj_methods[cl_i]->uf_name)
+				== 0)
+			{
+			    int *table = (int *)(if2cl + 1);
+			    table[if_i] = method_offset + cl_i;
+			    done = TRUE;
+			    break;
+			}
+		    }
+		    method_offset += parent->class_obj_method_count_child;
+		    parent = parent->class_extends;
+		}
+	    }
+
+	    if (!done)
+	    {
+		int *table = (int *)(if2cl + 1);
+		table[if_i] = pobj_method_offset + if_i;
+	    }
+	}
+    }
+
+    return OK;
+}
+
+/*
  * Handle ":class" and ":abstract class" up to ":endclass".
  * Handle ":interface" up to ":endinterface".
  */
@@ -534,6 +645,7 @@
 		{
 		    emsg(_(e_cannot_define_new_function_in_abstract_class));
 		    success = FALSE;
+		    func_clear_free(uf, FALSE);
 		    break;
 		}
 		garray_T *fgap = has_static || is_new
@@ -654,6 +766,8 @@
 	    {
 		semsg(_(e_not_valid_interface_str), impl);
 		success = FALSE;
+		clear_tv(&tv);
+		break;
 	    }
 
 	    class_T *ifcl = tv.vval.v_class;
@@ -839,83 +953,35 @@
 
 	if (cl->class_interface_count > 0 || extends_cl != NULL)
 	{
-	    // For each interface add a lookuptable for the member index on the
-	    // interface to the member index in this class.
-	    // And a lookuptable for the object method index on the interface
+	    // For each interface add a lookup table for the member index on
+	    // the interface to the member index in this class.
+	    // And a lookup table for the object method index on the interface
 	    // to the object method index in this class.
-	    // Also do this for the extended class, if any.
-	    for (int i = 0; i <= cl->class_interface_count; ++i)
+	    for (int i = 0; i < cl->class_interface_count; ++i)
 	    {
-		class_T *ifcl = i < cl->class_interface_count
-					    ? cl->class_interfaces_cl[i]
-					    : extends_cl;
-		if (ifcl == NULL)
-		    continue;
+		class_T *ifcl = cl->class_interfaces_cl[i];
 
-		// Table for members.
-		itf2class_T *if2cl = alloc_clear(sizeof(itf2class_T)
-				 + ifcl->class_obj_member_count * sizeof(int));
-		if (if2cl == NULL)
+		if (update_member_method_lookup_table(ifcl, cl, &objmethods,
+			    0, TRUE) == FAIL)
 		    goto cleanup;
-		if2cl->i2c_next = ifcl->class_itf2class;
-		ifcl->class_itf2class = if2cl;
-		if2cl->i2c_class = cl;
-		if2cl->i2c_is_method = FALSE;
+	    }
 
-		for (int if_i = 0; if_i < ifcl->class_obj_member_count; ++if_i)
-		    for (int cl_i = 0; cl_i < cl->class_obj_member_count;
-									++cl_i)
-		    {
-			if (STRCMP(ifcl->class_obj_members[if_i].ocm_name,
-				    cl->class_obj_members[cl_i].ocm_name) == 0)
-			{
-			    int *table = (int *)(if2cl + 1);
-			    table[if_i] = cl_i;
-			    break;
-			}
-		    }
+	    // Update the lookup table for the extended class, if nay
+	    if (extends_cl != NULL)
+	    {
+		class_T		*pclass = extends_cl;
+		int		pobj_method_offset = objmethods.ga_len;
 
-		// Table for methods.
-		if2cl = alloc_clear(sizeof(itf2class_T)
-				 + ifcl->class_obj_method_count * sizeof(int));
-		if (if2cl == NULL)
-		    goto cleanup;
-		if2cl->i2c_next = ifcl->class_itf2class;
-		ifcl->class_itf2class = if2cl;
-		if2cl->i2c_class = cl;
-		if2cl->i2c_is_method = TRUE;
-
-		for (int if_i = 0; if_i < ifcl->class_obj_method_count; ++if_i)
+		// Update the entire lineage of extended classes.
+		while (pclass != NULL)
 		{
-		    int done = FALSE;
-		    for (int cl_i = 0; cl_i < objmethods.ga_len; ++cl_i)
-		    {
-			if (STRCMP(ifcl->class_obj_methods[if_i]->uf_name,
-			       ((ufunc_T **)objmethods.ga_data)[cl_i]->uf_name)
-									  == 0)
-			{
-			    int *table = (int *)(if2cl + 1);
-			    table[if_i] = cl_i;
-			    done = TRUE;
-			    break;
-			}
-		    }
+		    if (update_member_method_lookup_table(pclass, cl,
+				&objmethods, pobj_method_offset, FALSE) ==
+			    FAIL)
+			goto cleanup;
 
-		    if (!done && extends_cl != NULL)
-		    {
-			for (int cl_i = 0;
-			     cl_i < extends_cl->class_obj_method_count; ++cl_i)
-			{
-			    if (STRCMP(ifcl->class_obj_methods[if_i]->uf_name,
-				   extends_cl->class_obj_methods[cl_i]->uf_name)
-									  == 0)
-			    {
-				int *table = (int *)(if2cl + 1);
-				table[if_i] = objmethods.ga_len + cl_i;
-				break;
-			    }
-			}
-		    }
+		    pobj_method_offset += pclass->class_obj_method_count_child;
+		    pclass = pclass->class_extends;
 		}
 	    }
 	}