Migration: fix handling of `.call` on a function-typed expression.

Change-Id: Ibdee57d455215cd974ea34c8aeab578bd801f87b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/170361
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/nnbd_migration/lib/src/edge_builder.dart b/pkg/nnbd_migration/lib/src/edge_builder.dart
index d10f7f4..35f98a8 100644
--- a/pkg/nnbd_migration/lib/src/edge_builder.dart
+++ b/pkg/nnbd_migration/lib/src/edge_builder.dart
@@ -1236,17 +1236,27 @@
       targetType = _thisOrSuper(node);
     }
     DecoratedType expressionType;
-    if (callee == null) {
+    DecoratedType calleeType;
+    if (targetType != null &&
+        targetType.type is FunctionType &&
+        node.methodName.name == 'call') {
+      // If `X` has a function type, then in the expression `X.call()`, the
+      // function being called is `X` itself, so the callee type is simply the
+      // type of `X`.
+      calleeType = targetType;
+    } else if (callee != null) {
+      calleeType = getOrComputeElementType(callee, targetType: targetType);
+      if (callee is PropertyAccessorElement) {
+        calleeType = calleeType.returnType;
+      }
+    }
+    if (calleeType == null) {
       // Dynamic dispatch.  The return type is `dynamic`.
       // TODO(paulberry): would it be better to assume a return type of `Never`
       // so that we don't unnecessarily propagate nullabilities everywhere?
       _dispatch(node.argumentList);
       expressionType = _makeNullableDynamicType(node);
     } else {
-      var calleeType = getOrComputeElementType(callee, targetType: targetType);
-      if (callee is PropertyAccessorElement) {
-        calleeType = calleeType.returnType;
-      }
       expressionType = _handleInvocationArguments(
           node,
           node.argumentList.arguments,
diff --git a/pkg/nnbd_migration/test/api_test.dart b/pkg/nnbd_migration/test/api_test.dart
index b0ba46d..a444f76 100644
--- a/pkg/nnbd_migration/test/api_test.dart
+++ b/pkg/nnbd_migration/test/api_test.dart
@@ -5231,6 +5231,24 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  Future<void> test_null_aware_call_followed_by_if_null() async {
+    var content = '''
+typedef MapGetter = Map<String, String> Function();
+void f(Map<String, String> m) {}
+void g(MapGetter/*?*/ mapGetter) {
+  f(mapGetter?.call() ?? {});
+}
+''';
+    var expected = '''
+typedef MapGetter = Map<String, String> Function();
+void f(Map<String, String> m) {}
+void g(MapGetter? mapGetter) {
+  f(mapGetter?.call() ?? {});
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   Future<void> test_null_aware_getter_invocation() async {
     var content = '''
 bool f(int i) => i?.isEven;
diff --git a/pkg/nnbd_migration/test/edge_builder_test.dart b/pkg/nnbd_migration/test/edge_builder_test.dart
index 0497d63..712cd2a 100644
--- a/pkg/nnbd_migration/test/edge_builder_test.dart
+++ b/pkg/nnbd_migration/test/edge_builder_test.dart
@@ -4692,6 +4692,29 @@
     expect(hasNullCheckHint(findNode.methodInvocation('c.m2')), isTrue);
   }
 
+  Future<void> test_methodInvocation_call_functionTyped() async {
+    await analyze('''
+void f(void Function(int x) callback, int y) => callback.call(y);
+''');
+    assertEdge(decoratedTypeAnnotation('int y').node,
+        decoratedTypeAnnotation('int x').node,
+        hard: true);
+  }
+
+  Future<void> test_methodInvocation_call_interfaceTyped() async {
+    // Make sure that we don't try to treat all methods called `call` as though
+    // the underlying type is a function type.
+    await analyze('''
+abstract class C {
+  void call(int x);
+}
+void f(C c, int y) => c.call(y);
+''');
+    assertEdge(decoratedTypeAnnotation('int y').node,
+        decoratedTypeAnnotation('int x').node,
+        hard: true);
+  }
+
   Future<void> test_methodInvocation_dynamic() async {
     await analyze('''
 class C {