Add external extension methods.

CFE transformation to add a function body for external
extension methods. Routes to `js_util.callMethod` with the
arguments in a ListLiteral to then be further optimized to
the `callMethodUnchecked` version if possible.

Change-Id: Iccdda7b8c16c19b37e20b7cbceb8c32498e64d3b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/213721
Commit-Queue: Riley Porter <rileyporter@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
diff --git a/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart b/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart
index dacab35..cb04456 100644
--- a/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart
+++ b/pkg/_js_interop_checks/lib/src/transformations/js_util_optimizer.dart
@@ -88,19 +88,17 @@
   @override
   visitProcedure(Procedure node) {
     _staticTypeContext.enterMember(node);
-    // Getters, setters, and fields declared as `static` will be skipped,
-    // because they do not have a node.kind of ProcedureKind.Method.
-    if (node.isExternal &&
-        node.isExtensionMember &&
-        node.kind == ProcedureKind.Method) {
+    if (node.isExternal && node.isExtensionMember && !_isDeclaredStatic(node)) {
       var transformedBody;
       if (api.getExtensionMemberKind(node) == ProcedureKind.Getter) {
         transformedBody = _getExternalGetterBody(node);
       } else if (api.getExtensionMemberKind(node) == ProcedureKind.Setter) {
         transformedBody = _getExternalSetterBody(node);
+      } else if (api.getExtensionMemberKind(node) == ProcedureKind.Method) {
+        transformedBody = _getExternalMethodBody(node);
       }
-      // TODO(rileyporter): Add transformation for external extension methods,
-      // static members, and any operators we decide to support.
+      // TODO(rileyporter): Add transformation for static members and any
+      // operators we decide to support.
       if (transformedBody != null) {
         node.function.body = transformedBody;
         node.isExternal = false;
@@ -149,13 +147,50 @@
         _lowerSetProperty(setPropertyInvocation), function.returnType));
   }
 
+  /// Returns a new function body for the given [node] external method.
+  ///
+  /// The new function body will call the optimized version of
+  /// `js_util.callMethod` for the given external method.
+  ReturnStatement _getExternalMethodBody(Procedure node) {
+    var function = node.function;
+    var callMethodInvocation = StaticInvocation(
+        _callMethodTarget,
+        Arguments([
+          VariableGet(function.positionalParameters.first),
+          StringLiteral(_getMemberName(node)),
+          ListLiteral(function.positionalParameters
+              .sublist(1)
+              .map((argument) => VariableGet(argument))
+              .toList())
+        ]))
+      ..fileOffset = node.fileOffset;
+    return ReturnStatement(AsExpression(
+        _lowerCallMethod(callMethodInvocation), function.returnType));
+  }
+
   /// Returns the member name, either from the `@JS` annotation if non-empty,
   /// or parsed from CFE generated node name.
   String _getMemberName(Procedure node) {
     var jsAnnotationName = getJSName(node);
-    return jsAnnotationName.isNotEmpty
-        ? jsAnnotationName
-        : node.name.text.substring(node.name.text.indexOf('#') + 1);
+    if (jsAnnotationName.isNotEmpty) {
+      return jsAnnotationName;
+    }
+    // TODO(rileyporter): Switch to using the ExtensionMemberDescriptor data.
+    var nodeName = node.name.text;
+    if (nodeName.contains('#')) {
+      return nodeName.substring(nodeName.indexOf('#') + 1);
+    } else {
+      return nodeName.substring(nodeName.indexOf('|') + 1);
+    }
+  }
+
+  /// Returns whether the given extension [node] is declared as `static`.
+  ///
+  /// All extension members have `isStatic` true, but the members declared as
+  /// static will not have a synthesized `this` variable.
+  bool _isDeclaredStatic(Procedure node) {
+    return node.function.positionalParameters.isEmpty ||
+        node.function.positionalParameters.first.name != '#this';
   }
 
   /// Replaces js_util method calls with optimization when possible.
diff --git a/tests/lib/js/external_extension_members_test.dart b/tests/lib/js/external_extension_members_test.dart
index 6c9caa2..823861c 100644
--- a/tests/lib/js/external_extension_members_test.dart
+++ b/tests/lib/js/external_extension_members_test.dart
@@ -33,6 +33,14 @@
   external set setter(_);
   @JS('setterAnnotation')
   external set annotatedSetter(_);
+
+  external num getField();
+  @JS('toString')
+  external String extToString();
+  external dynamic getFirstEl(list);
+  external num sumFn(a, b);
+  @JS('sumFn')
+  external num otherSumFn(a, b);
 }
 
 @JS('module.Bar')
@@ -56,6 +64,22 @@
       this.getterAnnotation = a;
     }
 
+    Foo.prototype.toString = function() {
+      return "Foo: " + this.field;
+    }
+
+    Foo.prototype.getField = function() {
+      return this.field;
+    }
+
+    Foo.prototype.getFirstEl = function(list) {
+      return list[0];
+    }
+
+    Foo.prototype.sumFn = function(a, b) {
+      return a + b;
+    }
+
     var module = {Bar: Foo};
     """);
 
@@ -94,6 +118,16 @@
     expect(js_util.getProperty(foo, 'setterAnnotation'), equals('whale'));
   });
 
+  test('methods', () {
+    var foo = Foo(42);
+
+    expect(foo.getField(), equals(42));
+    expect(foo.extToString(), equals('Foo: 42'));
+    expect(foo.getFirstEl([1, 2, 3]), equals(1));
+    expect(foo.sumFn(2, 3), equals(5));
+    expect(foo.otherSumFn(10, 5), equals(15));
+  });
+
   test('module class', () {
     var bar = Bar(5);
     expect(js_util.getProperty(bar, 'fieldAnnotation'), equals(5));
diff --git a/tests/lib_2/js/external_extension_members_test.dart b/tests/lib_2/js/external_extension_members_test.dart
index a2c08a1..b1b0cfe 100644
--- a/tests/lib_2/js/external_extension_members_test.dart
+++ b/tests/lib_2/js/external_extension_members_test.dart
@@ -30,6 +30,14 @@
   external set setter(_);
   @JS('setterAnnotation')
   external set annotatedSetter(_);
+
+  external num getField();
+  @JS('toString')
+  external String extToString();
+  external dynamic getFirstEl(list);
+  external num sumFn(a, b);
+  @JS('sumFn')
+  external num otherSumFn(a, b);
 }
 
 @JS('module.Bar')
@@ -55,6 +63,22 @@
       this.getterAnnotation = a;
     }
 
+    Foo.prototype.toString = function() {
+      return "Foo: " + this.field;
+    }
+
+    Foo.prototype.getField = function() {
+      return this.field;
+    }
+
+    Foo.prototype.getFirstEl = function(list) {
+      return list[0];
+    }
+
+    Foo.prototype.sumFn = function(a, b) {
+      return a + b;
+    }
+
     var module = {Bar: Foo};
     """);
 
@@ -76,6 +100,16 @@
     expect(js_util.getProperty(foo, 'setterAnnotation'), equals('whale'));
   });
 
+  test('methods', () {
+    var foo = Foo(42);
+
+    expect(foo.getField(), equals(42));
+    expect(foo.extToString(), equals('Foo: 42'));
+    expect(foo.getFirstEl([1, 2, 3]), equals(1));
+    expect(foo.sumFn(2, 3), equals(5));
+    expect(foo.otherSumFn(10, 5), equals(15));
+  });
+
   test('module class', () {
     var bar = Bar(5);
     expect(js_util.getProperty(bar, 'fieldAnnotation'), equals(5));