Version 2.14.0-84.0.dev

Merge commit '2efbd99c4799b087669b284c28e6644ff8f84807' into 'dev'
diff --git a/pkg/analysis_server/test/tool/completion_metrics/metrics_util_test.dart b/pkg/analysis_server/test/tool/completion_metrics/metrics_util_test.dart
index 8c83b04..265ac05 100644
--- a/pkg/analysis_server/test/tool/completion_metrics/metrics_util_test.dart
+++ b/pkg/analysis_server/test/tool/completion_metrics/metrics_util_test.dart
@@ -76,6 +76,36 @@
     });
   });
 
+  group('DistributionComputer', () {
+    test('displayString', () {
+      var computer = DistributionComputer();
+      expect(
+          computer.displayString(),
+          '[0] 0 [10] 0 [20] 0 [30] 0 [40] 0 [50] 0 '
+          '[60] 0 [70] 0 [80] 0 [90] 0 [100] 0');
+
+      for (var value in [
+        3, // 0-9
+        12, 15, // 10-19
+        23, 24, 26, // 20-29
+        30, 31, 31, 35, // 30-39
+        42, 42, 42, 42, 42, // 40-49
+        52, 53, 54, 55, 56, 57, // 50-59
+        63, // 60-69
+        72, 79, // 70-79
+        83, 84, 86, // 80-89
+        90, 91, 91, 99, // 90-99
+        100, 110, 120, 5000, // 100+
+      ]) {
+        computer.addValue(value);
+      }
+      expect(
+          computer.displayString(),
+          '[0] 1 [10] 2 [20] 3 [30] 4 [40] 5 [50] 6 '
+          '[60] 1 [70] 2 [80] 3 [90] 4 [100] 4');
+    });
+  });
+
   group('MeanReciprocalRankComputer', () {
     test('empty', () {
       var computer = MeanReciprocalRankComputer('');
diff --git a/pkg/analysis_server/tool/code_completion/completion_metrics.dart b/pkg/analysis_server/tool/code_completion/completion_metrics.dart
index 99eea81..434a3c1 100644
--- a/pkg/analysis_server/tool/code_completion/completion_metrics.dart
+++ b/pkg/analysis_server/tool/code_completion/completion_metrics.dart
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:convert';
-import 'dart:io' as io;
+import 'dart:developer';
 import 'dart:math' as math;
 
 import 'package:_fe_analyzer_shared/src/base/syntactic_entity.dart';
@@ -62,7 +62,7 @@
   var result = parser.parse(args);
 
   if (!validArguments(parser, result)) {
-    io.exit(1);
+    return;
   }
 
   var options = CompletionMetricsOptions(result);
@@ -98,7 +98,7 @@
   print('Analyzing root: "$rootPath"');
   var stopwatch = Stopwatch()..start();
   var computer = CompletionMetricsComputer(rootPath, options);
-  var code = await computer.computeMetrics();
+  await computer.computeMetrics();
   stopwatch.stop();
 
   var duration = Duration(milliseconds: stopwatch.elapsedMilliseconds);
@@ -113,7 +113,6 @@
   } else {
     computer.printResults();
   }
-  io.exit(code);
 }
 
 /// A [Counter] to track the performance of each of the completion strategies
@@ -282,6 +281,9 @@
   /// The function to be executed when this metrics collector is disabled.
   final void Function()? disableFunction;
 
+  /// The tag used to profile performance of completions for this set of metrics.
+  final UserTag userTag;
+
   final Counter completionCounter = Counter('all completions');
 
   final Counter completionMissedTokenCounter =
@@ -296,6 +298,8 @@
   final ArithmeticMeanComputer meanCompletionMS =
       ArithmeticMeanComputer('ms per completion');
 
+  final DistributionComputer distributionCompletionMS = DistributionComputer();
+
   final MeanReciprocalRankComputer mrrComputer =
       MeanReciprocalRankComputer('all completions');
 
@@ -333,7 +337,8 @@
   CompletionMetrics(this.name,
       {required this.availableSuggestions,
       this.enableFunction,
-      this.disableFunction});
+      this.disableFunction})
+      : userTag = UserTag(name);
 
   /// Return an instance extracted from the decoded JSON [map].
   factory CompletionMetrics.fromJson(Map<String, dynamic> map) {
@@ -349,6 +354,8 @@
         .fromJson(map['completionElementKindCounter'] as Map<String, dynamic>);
     metrics.meanCompletionMS
         .fromJson(map['meanCompletionMS'] as Map<String, dynamic>);
+    metrics.distributionCompletionMS
+        .fromJson(map['distributionCompletionMS'] as Map<String, dynamic>);
     metrics.mrrComputer.fromJson(map['mrrComputer'] as Map<String, dynamic>);
     metrics.successfulMrrComputer
         .fromJson(map['successfulMrrComputer'] as Map<String, dynamic>);
@@ -401,6 +408,7 @@
     completionKindCounter.addData(metrics.completionKindCounter);
     completionElementKindCounter.addData(metrics.completionElementKindCounter);
     meanCompletionMS.addData(metrics.meanCompletionMS);
+    distributionCompletionMS.addData(metrics.distributionCompletionMS);
     mrrComputer.addData(metrics.mrrComputer);
     successfulMrrComputer.addData(metrics.successfulMrrComputer);
     for (var entry in metrics.groupMrrComputers.entries) {
@@ -471,6 +479,7 @@
       'completionKindCounter': completionKindCounter.toJson(),
       'completionElementKindCounter': completionElementKindCounter.toJson(),
       'meanCompletionMS': meanCompletionMS.toJson(),
+      'distributionCompletionMS': distributionCompletionMS.toJson(),
       'mrrComputer': mrrComputer.toJson(),
       'successfulMrrComputer': successfulMrrComputer.toJson(),
       'groupMrrComputers': groupMrrComputers
@@ -542,6 +551,7 @@
   /// Record this elapsed ms count for the average ms count.
   void _recordTime(CompletionResult result) {
     meanCompletionMS.addValue(result.elapsedMS);
+    distributionCompletionMS.addValue(result.elapsedMS);
   }
 
   /// If the [result] is worse than any previously recorded results, record it.
@@ -568,9 +578,6 @@
 
   late ResolvedUnitResult _resolvedUnitResult;
 
-  /// The int to be returned from the [computeMetrics] call.
-  int resultCode = 0;
-
   /// A list of the metrics to be computed.
   final List<CompletionMetrics> targetMetrics = [];
 
@@ -622,8 +629,7 @@
     }
   }
 
-  Future<int> computeMetrics() async {
-    resultCode = 0;
+  Future<void> computeMetrics() async {
     // To compare two or more changes to completions, add a `CompletionMetrics`
     // object with enable and disable functions to the list of `targetMetrics`.
     targetMetrics.add(CompletionMetrics('shipping',
@@ -647,7 +653,6 @@
     for (var context in collection.contexts) {
       await _computeInContext(context.contextRoot);
     }
-    return resultCode;
   }
 
   int forEachExpectedCompletion(
@@ -791,6 +796,9 @@
       printCounter(metrics.completionElementKindCounter);
     }
 
+    var distribution = metrics.distributionCompletionMS.displayString();
+    print('${metrics.name}: $distribution');
+
     List<String> toRow(MeanReciprocalRankComputer computer) {
       return [
         computer.name,
@@ -948,6 +956,11 @@
 
     printHeading(2, 'Comparison of other metrics');
     printTable(table);
+
+    for (var metrics in targetMetrics) {
+      var distribution = metrics.distributionCompletionMS.displayString();
+      print('${metrics.name}: $distribution');
+    }
   }
 
   void printResults() {
@@ -1140,7 +1153,6 @@
             print('File $filePath skipped due to errors such as:');
             print('  ${analysisError.toString()}');
             print('');
-            resultCode = 1;
             continue;
           }
 
@@ -1214,9 +1226,11 @@
 
             var bestRank = -1;
             var bestName = '';
+            var defaultTag = getCurrentTag();
             for (var metrics in targetMetrics) {
               // Compute the completions.
               metrics.enable();
+              metrics.userTag.makeCurrent();
               // if (FeatureComputer.noDisabledFeatures) {
               //   var line = expectedCompletion.lineNumber;
               //   var column = expectedCompletion.columnNumber;
@@ -1229,6 +1243,7 @@
                 bestRank = rank;
                 bestName = metrics.name;
               }
+              defaultTag.makeCurrent();
               metrics.disable();
             }
             rankComparison.count(bestName);
@@ -1243,7 +1258,6 @@
           print('Exception caught analyzing: $filePath');
           print(exception.toString());
           print(stackTrace);
-          resultCode = 1;
         }
       }
     }
diff --git a/pkg/analysis_server/tool/code_completion/metrics_util.dart b/pkg/analysis_server/tool/code_completion/metrics_util.dart
index c3b01717..ab038b0 100644
--- a/pkg/analysis_server/tool/code_completion/metrics_util.dart
+++ b/pkg/analysis_server/tool/code_completion/metrics_util.dart
@@ -50,10 +50,6 @@
     max = map['max'] as int?;
   }
 
-  void printMean() {
-    print('Mean \'$name\' ${mean.toStringAsFixed(6)} (total = $count)');
-  }
-
   /// Return a map used to represent this computer in a JSON structure.
   Map<String, dynamic> toJson() {
     return {
@@ -174,6 +170,52 @@
   }
 }
 
+class DistributionComputer {
+  /// The buckets in which values are counted: [0..9], [10..19], ... [100..].
+  List<int> buckets = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+
+  /// Add the data from the given [computer] to this computer.
+  void addData(DistributionComputer computer) {
+    for (var i = 0; i < buckets.length; i++) {
+      buckets[i] += computer.buckets[i];
+    }
+  }
+
+  /// Add a millisecond value to the list of buckets.
+  void addValue(int value) {
+    var bucket = math.min(value ~/ 10, buckets.length - 1);
+    buckets[bucket]++;
+  }
+
+  /// Return a textual representation of the distribution.
+  String displayString() {
+    var buffer = StringBuffer();
+    for (var i = 0; i < buckets.length; i++) {
+      if (i > 0) {
+        buffer.write(' ');
+      }
+      buffer.write('[');
+      buffer.write(i * 10);
+      buffer.write('] ');
+      buffer.write(buckets[i]);
+    }
+    return buffer.toString();
+  }
+
+  /// Set the state of this computer to the state recorded in the decoded JSON
+  /// [map].
+  void fromJson(Map<String, dynamic> map) {
+    buckets = map['buckets'] as List<int>;
+  }
+
+  /// Return a map used to represent this computer in a JSON structure.
+  Map<String, dynamic> toJson() {
+    return {
+      'buckets': buckets,
+    };
+  }
+}
+
 /// A computer for the mean reciprocal rank. The MRR as well as the MRR only
 /// if the item was in the top 5 in the list see [MAX_RANK], is computed.
 /// https://en.wikipedia.org/wiki/Mean_reciprocal_rank.
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
 
diff --git a/tools/VERSION b/tools/VERSION
index 6e2781c..f487dbc 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 14
 PATCH 0
-PRERELEASE 83
+PRERELEASE 84
 PRERELEASE_PATCH 0
\ No newline at end of file