[wildcards] ensure bound `__` underscores are suggested in completions

See: https://github.com/dart-lang/sdk/issues/56361

Change-Id: Ia1b1a0fef17fb68f476d2ade96a0b5761152336b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/378720
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Phil Quitslund <pquitslund@google.com>
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/declaration_helper.dart b/pkg/analysis_server/lib/src/services/completion/dart/declaration_helper.dart
index f2e2b11..7394dce 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/declaration_helper.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/declaration_helper.dart
@@ -26,10 +26,6 @@
 /// A helper class that produces candidate suggestions for all of the
 /// declarations that are in scope at the completion location.
 class DeclarationHelper {
-  /// The regular expression used to detect an unused identifier (a sequence of
-  /// one or more underscores with no other characters).
-  static final RegExp UnusedIdentifier = RegExp(r'^_+$');
-
   /// The completion request being processed.
   final DartCompletionRequest request;
 
@@ -667,7 +663,7 @@
     required String? prefix,
   }) {
     // Don't suggest declarations in wildcard prefixed namespaces.
-    if (prefix == '_') return;
+    if (_isWildcard(prefix)) return;
 
     var importData = ImportData(
       libraryUri: library.source.uri,
@@ -1392,9 +1388,8 @@
     return true;
   }
 
-  /// Returns `true` if the [identifier] is composed of one or more underscore
-  /// characters and nothing else.
-  bool _isUnused(String identifier) => UnusedIdentifier.hasMatch(identifier);
+  /// Returns `true` if the [identifier] is a wildcard (a single `_`).
+  bool _isWildcard(String? identifier) => identifier == '_';
 
   /// Record that the given [operation] should be performed in the second pass.
   void _recordOperation(NotImportedOperation operation) {
@@ -1631,7 +1626,7 @@
         return;
       }
       // Don't suggest wildcard local functions.
-      if (element.name == '_') return;
+      if (_isWildcard(element.name)) return;
       var matcherScore = state.matcher.score(element.displayName);
       if (matcherScore != -1) {
         var suggestion = LocalFunctionSuggestion(
@@ -1705,7 +1700,7 @@
   /// Adds a suggestion for the parameter represented by the [element].
   void _suggestParameter(ParameterElement element) {
     if (visibilityTracker.isVisible(element: element, importData: null)) {
-      if (mustBeConstant || _isUnused(element.name)) {
+      if (mustBeConstant || _isWildcard(element.name)) {
         return;
       }
       var matcherScore = state.matcher.score(element.displayName);
diff --git a/pkg/analysis_server/test/services/completion/dart/declaration/wildcard_variables_test.dart b/pkg/analysis_server/test/services/completion/dart/declaration/wildcard_variables_test.dart
index e880934..ff191d4 100644
--- a/pkg/analysis_server/test/services/completion/dart/declaration/wildcard_variables_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/declaration/wildcard_variables_test.dart
@@ -65,7 +65,7 @@
 @reflectiveTest
 class WildcardForLoopTest extends AbstractCompletionDriverTest {
   @override
-  Set<String> allowedIdentifiers = {'_'};
+  Set<String> allowedIdentifiers = {'_', '__'};
 
   @override
   bool get includeKeywords => false;
@@ -85,6 +85,23 @@
 ''');
   }
 
+  Future<void> test_forEach_argumentList_underscores() async {
+    await computeSuggestions('''
+void p(Object o) {}
+
+void f() {
+  for (var __ in []) {
+    p(^);
+  }
+}
+''');
+    assertResponse('''
+suggestions
+  __
+    kind: localVariable
+''');
+  }
+
   Future<void> test_forParts_argumentList() async {
     await computeSuggestions('''
 void p(Object o) {}
@@ -99,12 +116,29 @@
 suggestions
 ''');
   }
+
+  Future<void> test_forParts_argumentList_underscores() async {
+    await computeSuggestions('''
+void p(Object o) {}
+
+void f() {
+  for (var __ = 0; ;) {
+    p(^);
+  }
+}
+''');
+    assertResponse('''
+suggestions
+  __
+    kind: localVariable
+''');
+  }
 }
 
 @reflectiveTest
 class WildcardImportPrefixTest extends AbstractCompletionDriverTest {
   @override
-  Set<String> allowedIdentifiers = {'_', 'isBlank'};
+  Set<String> allowedIdentifiers = {'_', '__', 'isBlank'};
 
   @override
   bool get includeKeywords => false;
@@ -131,6 +165,31 @@
 ''');
   }
 
+  Future<void> test_argumentList_underscores() async {
+    newFile('$testPackageLibPath/ext.dart', '''
+extension ES on String {
+  bool get isBlank => false;
+}
+''');
+
+    await computeSuggestions('''
+import 'ext.dart' as __;
+
+void p(Object o) {}
+
+void f() {
+  p(^);
+}
+''');
+    assertResponse('''
+suggestions
+  __.ES
+    kind: extensionInvocation
+  __
+    kind: library
+''');
+  }
+
   Future<void> test_stringExtension_argumentList() async {
     newFile('$testPackageLibPath/ext.dart', '''
 extension ES on String {
@@ -158,7 +217,7 @@
 @reflectiveTest
 class WildcardLocalVariableTest extends AbstractCompletionDriverTest {
   @override
-  Set<String> allowedIdentifiers = {'_', 'b'};
+  Set<String> allowedIdentifiers = {'_', '__', 'b'};
 
   @override
   bool get includeKeywords => false;
@@ -192,12 +251,28 @@
 suggestions
 ''');
   }
+
+  Future<void> test_argumentList_underscores() async {
+    await computeSuggestions('''
+void p(Object o) {}
+
+void f() {
+  var __ = 0;
+  p(^);
+}
+''');
+    assertResponse(r'''
+suggestions
+  __
+    kind: localVariable
+''');
+  }
 }
 
 @reflectiveTest
 class WildcardParameterTest extends AbstractCompletionDriverTest {
   @override
-  Set<String> allowedIdentifiers = {'_', 'b'};
+  Set<String> allowedIdentifiers = {'_', '__', 'b'};
 
   @override
   bool get includeKeywords => false;
@@ -216,6 +291,21 @@
     kind: parameter
 ''');
   }
+
+  Future<void> test_argumentList_underscores() async {
+    await computeSuggestions('''
+void p(Object o) {}
+
+void f(int __) {
+  p(^);
+}
+''');
+    assertResponse('''
+suggestions
+  __
+    kind: parameter
+''');
+  }
 }
 
 /// Top level variables are binding so not technically wildcards but look just