[analysis_server] Take prefixes into account when determining the visibility of completions

A prefixed identifier like `a.Foo` should not hide a prefixed identifier like `b.Foo` not an unprefixed identifier (and vice-versa).

Fixes https://github.com/Dart-Code/Dart-Code/issues/5242

Change-Id: I8be8b3260f2a9f4333d38bba7ffe309cd3817932
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/383641
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Keerti Parthasarathy <keertip@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/visibility_tracker.dart b/pkg/analysis_server/lib/src/services/completion/dart/visibility_tracker.dart
index 6a71a35..eca3029 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/visibility_tracker.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/visibility_tracker.dart
@@ -25,9 +25,15 @@
     var name = element?.displayName;
     if (name == null) {
       return false;
-    } else if (importData?.isNotImported ?? false) {
-      return !_declaredNames.contains(name);
     }
-    return _declaredNames.add(name);
+
+    var isNotImported = importData?.isNotImported ?? false;
+    var prefix = importData?.prefix;
+    var qualifiedName = prefix != null ? '$prefix.$name' : name;
+
+    if (isNotImported) {
+      return !_declaredNames.contains(qualifiedName);
+    }
+    return _declaredNames.add(qualifiedName);
   }
 }
diff --git a/pkg/analysis_server/test/completion_test_support.dart b/pkg/analysis_server/test/completion_test_support.dart
index 5bed7c2..2777810 100644
--- a/pkg/analysis_server/test/completion_test_support.dart
+++ b/pkg/analysis_server/test/completion_test_support.dart
@@ -18,7 +18,7 @@
       .toList();
 
   void assertHasCompletion(String completion,
-      {ElementKind? elementKind, bool? isDeprecated}) {
+      {ElementKind? elementKind, bool? isDeprecated, Matcher? libraryUri}) {
     var expectedOffset = completion.indexOf(CURSOR_MARKER);
     if (expectedOffset >= 0) {
       if (completion.contains(CURSOR_MARKER, expectedOffset + 1)) {
@@ -58,6 +58,9 @@
     if (isDeprecated != null) {
       expect(matchingSuggestion.isDeprecated, isDeprecated);
     }
+    if (libraryUri != null) {
+      expect(matchingSuggestion.libraryUri, libraryUri);
+    }
   }
 
   void assertHasNoCompletion(String completion) {
diff --git a/pkg/analysis_server/test/services/completion/dart/test_all.dart b/pkg/analysis_server/test/services/completion/dart/test_all.dart
index f8b9911..c669869 100644
--- a/pkg/analysis_server/test/services/completion/dart/test_all.dart
+++ b/pkg/analysis_server/test/services/completion/dart/test_all.dart
@@ -10,6 +10,7 @@
 import 'relevance/test_all.dart' as relevance_tests;
 import 'shadowing_test.dart' as shadowing_test;
 import 'text_expectations.dart';
+import 'visibility/test_all.dart' as visibility;
 
 void main() {
   defineReflectiveSuite(() {
@@ -18,6 +19,7 @@
     location.main();
     relevance_tests.main();
     shadowing_test.main();
+    visibility.main();
     defineReflectiveTests(UpdateTextExpectations);
   }, name: 'dart');
 }
diff --git a/pkg/analysis_server/test/services/completion/dart/visibility/test_all.dart b/pkg/analysis_server/test/services/completion/dart/visibility/test_all.dart
new file mode 100644
index 0000000..b69a5db
--- /dev/null
+++ b/pkg/analysis_server/test/services/completion/dart/visibility/test_all.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'types_test.dart' as types;
+
+void main() {
+  defineReflectiveSuite(() {
+    types.main();
+  }, name: 'visibility');
+}
diff --git a/pkg/analysis_server/test/services/completion/dart/visibility/types_test.dart b/pkg/analysis_server/test/services/completion/dart/visibility/types_test.dart
new file mode 100644
index 0000000..4df3e5c
--- /dev/null
+++ b/pkg/analysis_server/test/services/completion/dart/visibility/types_test.dart
@@ -0,0 +1,120 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../../../../completion_test_support.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(CompletionVisibilityTest);
+  });
+}
+
+@reflectiveTest
+class CompletionVisibilityTest extends CompletionTestCase {
+  Future<void> test_imported() async {
+    newFile(convertPath('$testPackageLibPath/lib.dart'), '''
+class C {}
+''');
+    await getTestCodeSuggestions('''
+import 'lib.dart';
+
+C^
+''');
+    assertHasCompletion('C');
+  }
+
+  Future<void> test_imported_localAndMultiplePrefixes() async {
+    newFile(convertPath('$testPackageLibPath/lib.dart'), '''
+class C {}
+''');
+    await getTestCodeSuggestions('''
+import 'lib.dart' as l1;
+import 'lib.dart' as l2;
+
+class C {}
+
+C^
+''');
+    assertHasCompletion('C');
+    assertHasCompletion('l1.C');
+    assertHasCompletion('l2.C');
+  }
+
+  Future<void> test_imported_localAndUnprefixed() async {
+    newFile(convertPath('$testPackageLibPath/lib.dart'), '''
+class C {}
+''');
+    await getTestCodeSuggestions('''
+import 'lib.dart';
+
+class C {}
+
+C^
+''');
+    // Verify exactly one, and it was the local one.
+    assertHasCompletion('C', libraryUri: isNull);
+  }
+
+  Future<void> test_imported_multiplePrefixes() async {
+    newFile(convertPath('$testPackageLibPath/lib.dart'), '''
+class C {}
+''');
+    await getTestCodeSuggestions('''
+import 'lib.dart' as l1;
+import 'lib.dart' as l2;
+
+C^
+''');
+    assertHasCompletion('l1.C');
+    assertHasCompletion('l2.C');
+  }
+
+  Future<void> test_imported_prefix() async {
+    newFile(convertPath('$testPackageLibPath/lib.dart'), '''
+class C {}
+''');
+    await getTestCodeSuggestions('''
+import 'lib.dart' as l;
+
+C^
+''');
+    assertHasCompletion('l.C');
+  }
+
+  Future<void> test_local() async {
+    await getTestCodeSuggestions('''
+class C {}
+
+C^
+''');
+    assertHasCompletion('C');
+  }
+
+  Future<void> test_localAndNotImported() async {
+    newFile(convertPath('$testPackageLibPath/lib.dart'), '''
+class C {}
+''');
+    await getTestCodeSuggestions('''
+class C {}
+
+C^
+''');
+    // This verifies exactly one, since we won't suggest the not-imported
+    // when it matches a local name.
+    assertHasCompletion('C');
+  }
+
+  Future<void> test_notImported() async {
+    newFile(convertPath('$testPackageLibPath/lib.dart'), '''
+class C {}
+''');
+    await getTestCodeSuggestions('''
+C^
+''');
+    assertHasCompletion('C');
+  }
+}