Issue 45887. Report AMBIGUOUS_EXTENSION_MEMBER_ACCESS with actually conflicting extensions.

Bug: https://github.com/dart-lang/sdk/issues/45887
Change-Id: I8ec587b4479e165056766d433cb0a3c459d5f504
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/198240
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analyzer/lib/src/dart/resolver/extension_member_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/extension_member_resolver.dart
index c5013b7..e78d21f 100644
--- a/pkg/analyzer/lib/src/dart/resolver/extension_member_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/extension_member_resolver.dart
@@ -23,6 +23,8 @@
 import 'package:analyzer/src/dart/resolver/scope.dart';
 import 'package:analyzer/src/error/codes.dart';
 import 'package:analyzer/src/generated/resolver.dart';
+import 'package:analyzer/src/util/either.dart';
+import 'package:analyzer/src/utilities/extensions/string.dart';
 
 class ExtensionMemberResolver {
   final ResolverVisitor _resolver;
@@ -64,22 +66,27 @@
       return extensions[0].asResolutionResult;
     }
 
-    var extension = _chooseMostSpecific(extensions);
-    if (extension != null) {
-      return extension.asResolutionResult;
-    }
-
-    _errorReporter.reportErrorForOffset(
-      CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS,
-      nameEntity.offset,
-      nameEntity.length,
-      [
-        name,
-        extensions[0].extension.name,
-        extensions[1].extension.name,
-      ],
+    var mostSpecific = _chooseMostSpecific(extensions);
+    return mostSpecific.map(
+      (extension) {
+        return extension.asResolutionResult;
+      },
+      (noneMoreSpecific) {
+        _errorReporter.reportErrorForOffset(
+          CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS,
+          nameEntity.offset,
+          nameEntity.length,
+          [
+            name,
+            noneMoreSpecific
+                .map((e) => "'${e.extension.name ?? '<unnamed>'}'")
+                .toList()
+                .commaSeparatedWithAnd,
+          ],
+        );
+        return ResolutionResult.ambiguous;
+      },
     );
-    return ResolutionResult.ambiguous;
   }
 
   /// Resolve the [name] (without `=`) to the corresponding getter and setter
@@ -250,27 +257,48 @@
     }
   }
 
-  /// Return the most specific extension or `null` if no single one can be
-  /// identified.
-  _InstantiatedExtension? _chooseMostSpecific(
-      List<_InstantiatedExtension> extensions) {
-    for (var i = 0; i < extensions.length; i++) {
-      var e1 = extensions[i];
-      var isMoreSpecific = true;
-      for (var j = 0; j < extensions.length; j++) {
-        var e2 = extensions[j];
-        if (i != j && !_isMoreSpecific(e1, e2)) {
-          isMoreSpecific = false;
-          break;
+  /// Return either the most specific extension, or a list of the extensions
+  /// that are ambiguous.
+  Either2<_InstantiatedExtension, List<_InstantiatedExtension>>
+      _chooseMostSpecific(List<_InstantiatedExtension> extensions) {
+    _InstantiatedExtension? bestSoFar;
+    var noneMoreSpecific = <_InstantiatedExtension>[];
+    for (var candidate in extensions) {
+      if (noneMoreSpecific.isNotEmpty) {
+        var isMostSpecific = true;
+        var hasMoreSpecific = false;
+        for (var other in noneMoreSpecific) {
+          if (!_isMoreSpecific(candidate, other)) {
+            isMostSpecific = false;
+          }
+          if (_isMoreSpecific(other, candidate)) {
+            hasMoreSpecific = true;
+          }
         }
-      }
-      if (isMoreSpecific) {
-        return e1;
+        if (isMostSpecific) {
+          bestSoFar = candidate;
+          noneMoreSpecific.clear();
+        } else if (!hasMoreSpecific) {
+          noneMoreSpecific.add(candidate);
+        }
+      } else if (bestSoFar == null) {
+        bestSoFar = candidate;
+      } else if (_isMoreSpecific(bestSoFar, candidate)) {
+        // already
+      } else if (_isMoreSpecific(candidate, bestSoFar)) {
+        bestSoFar = candidate;
+      } else {
+        noneMoreSpecific.add(bestSoFar);
+        noneMoreSpecific.add(candidate);
+        bestSoFar = null;
       }
     }
 
-    // Otherwise fail.
-    return null;
+    if (bestSoFar != null) {
+      return Either2.t1(bestSoFar);
+    } else {
+      return Either2.t2(noneMoreSpecific);
+    }
   }
 
   /// Return extensions for the [type] that match the given [name] in the
diff --git a/pkg/analyzer/lib/src/error/codes.dart b/pkg/analyzer/lib/src/error/codes.dart
index 9c8e2f7..96d81eb 100644
--- a/pkg/analyzer/lib/src/error/codes.dart
+++ b/pkg/analyzer/lib/src/error/codes.dart
@@ -241,19 +241,14 @@
   //   print(E2(s).charCount);
   // }
   // ```
-  /*
-   * TODO(brianwilkerson) This message doesn't handle the possible case where
-   *  there are more than 2 extensions, nor does it handle well the case where
-   *  one or more of the extensions is unnamed.
-   */
   static const CompileTimeErrorCode AMBIGUOUS_EXTENSION_MEMBER_ACCESS =
       CompileTimeErrorCode(
           'AMBIGUOUS_EXTENSION_MEMBER_ACCESS',
-          "A member named '{0}' is defined in extensions '{1}' and '{2}' and "
-              "neither is more specific.",
+          "A member named '{0}' is defined in extensions {1}, and "
+              "none are more specific.",
           correction:
               "Try using an extension override to specify the extension "
-              "you want to to be chosen.",
+              "you want to be chosen.",
           hasPublishedDocs: true);
 
   /**
diff --git a/pkg/analyzer/test/src/diagnostics/ambiguous_extension_member_access_test.dart b/pkg/analyzer/test/src/diagnostics/ambiguous_extension_member_access_test.dart
index 424912e..6355201 100644
--- a/pkg/analyzer/test/src/diagnostics/ambiguous_extension_member_access_test.dart
+++ b/pkg/analyzer/test/src/diagnostics/ambiguous_extension_member_access_test.dart
@@ -96,6 +96,56 @@
     assertTypeDynamic(access);
   }
 
+  test_method_conflict_conflict_notSpecific() async {
+    await assertErrorsInCode('''
+extension E1 on int { void foo() {} }
+extension E2 on int { void foo() {} }
+extension E on int? { void foo() {} }
+void f() {
+  0.foo();
+}
+''', [
+      error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 129, 3,
+          messageContains: "'E1' and 'E2'"),
+    ]);
+  }
+
+  test_method_conflict_conflict_specific() async {
+    await assertNoErrorsInCode('''
+extension E1 on int? { void foo() {} }
+extension E2 on int? { void foo() {} }
+extension E on int { void foo() {} }
+void f() {
+  0.foo();
+}
+''');
+  }
+
+  test_method_conflict_notSpecific_conflict() async {
+    await assertErrorsInCode('''
+extension E1 on int { void foo() {} }
+extension E on int? { void foo() {} }
+extension E2 on int { void foo() {} }
+void f() {
+  0.foo();
+}
+''', [
+      error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 129, 3,
+          messageContains: "'E1' and 'E2'"),
+    ]);
+  }
+
+  test_method_conflict_specific_conflict() async {
+    await assertNoErrorsInCode('''
+extension E1 on int? { void foo() {} }
+extension E on int { void foo() {} }
+extension E2 on int? { void foo() {} }
+void f() {
+  0.foo();
+}
+''');
+  }
+
   test_method_method() async {
     await assertErrorsInCode('''
 extension E1 on int {
@@ -117,6 +167,46 @@
     assertTypeDynamic(invocation);
   }
 
+  test_method_notSpecific_conflict_conflict() async {
+    await assertErrorsInCode('''
+extension E on int? { void foo() {} }
+extension E1 on int { void foo() {} }
+extension E2 on int { void foo() {} }
+void f() {
+  0.foo();
+}
+''', [
+      error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 129, 3,
+          messageContains: "'E1' and 'E2'"),
+    ]);
+  }
+
+  test_method_notSpecific_conflict_conflict_conflict() async {
+    await assertErrorsInCode('''
+extension E on int? { void foo() {} }
+extension E1 on int { void foo() {} }
+extension E2 on int { void foo() {} }
+extension E3 on int { void foo() {} }
+void f() {
+  0.foo();
+}
+''', [
+      error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 167, 3,
+          messageContains: "'E1', 'E2', and 'E3'"),
+    ]);
+  }
+
+  test_method_specific_conflict_conflict() async {
+    await assertNoErrorsInCode('''
+extension E on int { void foo() {} }
+extension E1 on int? { void foo() {} }
+extension E2 on int? { void foo() {} }
+void f() {
+  0.foo();
+}
+''');
+  }
+
   test_noMoreSpecificExtension() async {
     await assertErrorsInCode(r'''
 class Target<T> {}
diff --git a/pkg/analyzer/tool/diagnostics/diagnostics.md b/pkg/analyzer/tool/diagnostics/diagnostics.md
index 2ca67b4..1722c990 100644
--- a/pkg/analyzer/tool/diagnostics/diagnostics.md
+++ b/pkg/analyzer/tool/diagnostics/diagnostics.md
@@ -398,8 +398,8 @@
 
 ### ambiguous_extension_member_access
 
-_A member named '{0}' is defined in extensions '{1}' and '{2}' and neither is
-more specific._
+_A member named '{0}' is defined in extensions {1}, and neither is more
+specific._
 
 #### Description