[ddc] Improve NoSuchMethod error for closures

Changes the text from "NoSuchMethodError: ''" to
"NoSuchMethodError: '<anonymous closure>'" for some failed invocations.

Adds more test cases involving getter and field invocations.

Change-Id: I685772fc69c7216ae67ea2535de38c8b925c0809
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/425940
Commit-Queue: Nicholas Shahan <nshahan@google.com>
Reviewed-by: Mark Zhou <markzipan@google.com>
diff --git a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart
index 21bc20d..6a11674 100644
--- a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart
+++ b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart
@@ -553,10 +553,14 @@
   var originalTarget = JS<bool>('!', '# === void 0', obj) ? f : obj;
 
   callNSM(@notNull String errorMessage) {
+    var name = displayName;
+    if (name is String && name.isEmpty) {
+      name = '<anonymous closure>';
+    }
     return noSuchMethod(
       originalTarget,
       InvocationImpl(
-        displayName,
+        name,
         JS<List<Object?>>('!', '#', args),
         namedArguments: named,
         // Repeated the default value here in JS to preserve the historic
diff --git a/tests/dartdevc/no_such_method_errors_test.dart b/tests/dartdevc/no_such_method_errors_test.dart
index a7e7174..1dc87b6 100644
--- a/tests/dartdevc/no_such_method_errors_test.dart
+++ b/tests/dartdevc/no_such_method_errors_test.dart
@@ -41,6 +41,22 @@
     val += 10;
     return val.toString();
   };
+
+  Function get getterArity1 => (int val) {
+    val += 10;
+    return val.toString();
+  };
+
+  Function fieldArity1Tearoff = A.staticArity1;
+
+  Function get getterArity1Tearoff => A.staticArity1;
+}
+
+class B {
+  String call(int val) {
+    val += 10;
+    return val.toString();
+  }
 }
 
 String arity1(int val) {
@@ -55,6 +71,15 @@
   return val.toString();
 };
 
+Function get getterArity1 => (int val) {
+  val += 10;
+  return val.toString();
+};
+
+Function fieldArity1Tearoff = arity1;
+
+Function get getterArity1Tearoff => arity1;
+
 String requiredNamedArity1({required bool fosse}) {
   return fosse.toString();
 }
@@ -63,6 +88,19 @@
 
 void main() {
   group('Dynamic call of', () {
+    test('instance of class with a `call()` method', () {
+      dynamic d = B();
+      Expect.throws<NoSuchMethodError>(
+        () => d(),
+        (error) => error.toString().contains(
+          "NoSuchMethodError: 'call'\n"
+          "Dynamic call with missing positional arguments. "
+          "Expected: 1 Actual: 0\n"
+          "Receiver: Instance of 'B'\n"
+          "Arguments: []",
+        ),
+      );
+    });
     dynamic instanceOfA = A();
     test('instance of a class with no `call()` method', () {
       // Compiled as `dcall()`.
@@ -193,6 +231,8 @@
         // Compiled as `dgcall()` and throws from `checkAndCall()`.
         expectThrowsNSMWithExactError(
           () => instantiatedTearoff<int, double>(),
+          // TODO(60654): Improve error message to include the actual name of
+          // the method.
           "NoSuchMethodError: 'result'\n"
           "Dynamic call failed.\n"
           "Incorrect number of type arguments. "
@@ -277,6 +317,22 @@
         (error) => error.toString().contains("NoSuchMethodError: 'arity1'"),
       );
     });
+    test('class instance getter that returns tearoff', () {
+      Expect.throws<NoSuchMethodError>(
+        () => A().getterArity1Tearoff(),
+        (error) => error.toString().contains(
+          "NoSuchMethodError: 'getterArity1Tearoff'",
+        ),
+      );
+    });
+    test('class instance field that stores a tearoff', () {
+      Expect.throws<NoSuchMethodError>(
+        () => A().fieldArity1Tearoff(),
+        (error) => error.toString().contains(
+          "NoSuchMethodError: 'fieldArity1Tearoff'",
+        ),
+      );
+    });
     test('class instance generic method', () {
       dynamic instanceOfA = A();
       Expect.throws<NoSuchMethodError>(
@@ -297,10 +353,14 @@
       dynamic tearoff = A().genericArity2<int, String>;
       Expect.throws<NoSuchMethodError>(
         () => tearoff(10),
-        (error) => error.toString().contains("NoSuchMethodError: 'result'"),
+        (error) => error.toString().contains(
+          // TODO(60654): Improve error message to include the actual name of
+          // the method.
+          "NoSuchMethodError: 'result'",
+        ),
       );
     });
-    test('class instance field', () {
+    test('class instance field that stores a closure', () {
       dynamic instanceOfA = A();
       Expect.throws<NoSuchMethodError>(
         () => instanceOfA.fieldArity1(),
@@ -308,6 +368,14 @@
             error.toString().contains("NoSuchMethodError: 'fieldArity1'"),
       );
     });
+    test('class instance getter that stores a closure', () {
+      dynamic instanceOfA = A();
+      Expect.throws<NoSuchMethodError>(
+        () => instanceOfA.getterArity1(),
+        (error) =>
+            error.toString().contains("NoSuchMethodError: 'getterArity1'"),
+      );
+    });
     test('class static method tearoff', () {
       dynamic tearoff = A.staticArity1;
       Expect.throws<NoSuchMethodError>(
@@ -329,7 +397,11 @@
       dynamic tearoff = A.staticGenericArity2<int, double>;
       Expect.throws<NoSuchMethodError>(
         () => tearoff(10),
-        (error) => error.toString().contains("NoSuchMethodError: 'result'"),
+        (error) => error.toString().contains(
+          // TODO(60654): Improve error message to include the actual name of
+          // the method.
+          "NoSuchMethodError: 'result'",
+        ),
       );
     });
     test('top level method tearoff', () {
@@ -352,13 +424,39 @@
       dynamic tearoff = genericArity2<int, String>;
       Expect.throws<NoSuchMethodError>(
         () => tearoff(10),
-        (error) => error.toString().contains("NoSuchMethodError: 'result'"),
+        (error) => error.toString().contains(
+          // TODO(60654): Improve error message to include the actual name of
+          // the method.
+          "NoSuchMethodError: 'result'",
+        ),
       );
     });
-    test('top level field', () {
+    test('top level field storing a closure', () {
       Expect.throws<NoSuchMethodError>(
         () => fieldArity1(),
-        (error) => error.toString().contains("NoSuchMethodError: ''"),
+        (error) => error.toString().contains(
+          "NoSuchMethodError: '<anonymous closure>'",
+        ),
+      );
+    });
+    test('top level getter that returns a closure', () {
+      Expect.throws<NoSuchMethodError>(
+        () => getterArity1(),
+        (error) => error.toString().contains(
+          "NoSuchMethodError: '<anonymous closure>'",
+        ),
+      );
+    });
+    test('top level field storing a tearoff', () {
+      Expect.throws<NoSuchMethodError>(
+        () => fieldArity1Tearoff(),
+        (error) => error.toString().contains("NoSuchMethodError: 'arity1'"),
+      );
+    });
+    test('top level getter that returns a tearoff', () {
+      Expect.throws<NoSuchMethodError>(
+        () => getterArity1Tearoff(),
+        (error) => error.toString().contains("NoSuchMethodError: 'arity1'"),
       );
     });
   });