[analysis_server] Fix LSP implementations that are provided by mixins
Fixes https://github.com/Dart-Code/Dart-Code/issues/4531.
Change-Id: I1b02dcacab03590012401087e2c2c6f28f690df0
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/302400
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_implementation.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_implementation.dart
index b2fad84..91516bf 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_implementation.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_implementation.dart
@@ -66,25 +66,33 @@
"appendAllSubtypes",
(performance) => server.searchEngine
.appendAllSubtypes(interfaceElement, allSubtypes, performance));
+
final locations = performance.run(
"filter and get location",
(_) => allSubtypes
- .map((e) {
- int offset = e.nameOffset;
- int length = e.nameLength;
- if (needsMember) {
- // Filter based on type, so when searching for members we don't
- // include any intermediate classes that don't have
- // implementations for the method.
- var member = helper.findMemberElement(e)?.nonSynthetic;
- if (member == null) return null;
- offset = member.nameOffset;
- length = member.nameLength;
+ .map((element) {
+ return needsMember
+ // Filter based on type, so when searching for members we don't
+ // include any intermediate classes that don't have
+ // implementations for the method.
+ ? helper.findMemberElement(element)?.nonSynthetic
+ : element;
+ })
+ .whereNotNull()
+ .toSet()
+ .map((element) {
+ final unitElement =
+ element.thisOrAncestorOfType<CompilationUnitElement>();
+ if (unitElement == null) {
+ return null;
}
- var unitElement = e.enclosingElement;
return Location(
uri: Uri.file(unitElement.source.fullName),
- range: toRange(unitElement.lineInfo, offset, length),
+ range: toRange(
+ unitElement.lineInfo,
+ element.nameOffset,
+ element.nameLength,
+ ),
);
})
.whereNotNull()
diff --git a/pkg/analysis_server/test/lsp/implementation_test.dart b/pkg/analysis_server/test/lsp/implementation_test.dart
index 71cfac6..3c44401 100644
--- a/pkg/analysis_server/test/lsp/implementation_test.dart
+++ b/pkg/analysis_server/test/lsp/implementation_test.dart
@@ -3,9 +3,11 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/lsp_protocol/protocol.dart';
+import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
+import '../utils/test_code_extensions.dart';
import 'server_abstract.dart';
void main() {
@@ -17,30 +19,30 @@
@reflectiveTest
class ImplementationTest extends AbstractLspAnalysisServerTest {
Future<void> test_class_excludesSelf() => _testMarkedContent('''
- abstract class ^[[A]] {}
+ abstract class ^[!A!] {}
class B extends A {}
class C extends A {}
''', expectResults: false);
Future<void> test_class_excludesSuper() => _testMarkedContent('''
- abstract class [[A]] {}
+ abstract class [!A!] {}
class ^B extends A {}
class C extends A {}
''', expectResults: false);
Future<void> test_class_sub() => _testMarkedContent('''
abstract class ^A {}
- class [[B]] extends A {}
- class [[C]] extends A {}
+ class /*[0*/B/*0]*/ extends A {}
+ class /*[1*/C/*1]*/ extends A {}
''');
Future<void> test_class_subSub() => _testMarkedContent('''
abstract class ^A {}
- class [[B]] extends A {}
- class [[C]] extends A {}
- class [[D]] extends B {}
- class [[E]] extends B {}
- class [[F]] extends E {}
+ class /*[0*/B/*0]*/ extends A {}
+ class /*[1*/C/*1]*/ extends A {}
+ class /*[2*/D/*2]*/ extends B {}
+ class /*[3*/E/*3]*/ extends B {}
+ class /*[4*/F/*4]*/ extends E {}
''');
Future<void> test_emptyResults() async {
@@ -58,7 +60,7 @@
Future<void> test_getter_overriddenByField() => _testMarkedContent('''
class B extends A {
- final String? [[a]] = null;
+ final String? [!a!] = null;
}
abstract class A {
@@ -74,12 +76,12 @@
class B extends A {}
- class [[E]] extends B {}
+ class [!E!] extends B {}
''', expectResults: false);
Future<void> test_method_excludesSelf() => _testMarkedContent('''
abstract class A {
- void ^[[b]]();
+ void ^[!b!]();
}
class B extends A {
@@ -89,7 +91,7 @@
Future<void> test_method_excludesSuper() => _testMarkedContent('''
abstract class A {
- void [[b]]();
+ void [!b!]();
}
class B extends A {
@@ -103,21 +105,21 @@
}
class B extends A {
- void [[b]]() {}
+ void /*[0*/b/*0]*/() {}
}
class C extends A {
- void [[b]]() {}
+ void /*[1*/b/*1]*/() {}
}
class D extends B {
- void [[b]]() {}
+ void /*[2*/b/*2]*/() {}
}
class E extends B {}
class F extends E {
- void [[b]]() {}
+ void /*[3*/b/*3]*/() {}
}
void fromCallSite() {
@@ -132,7 +134,7 @@
}
class B extends A {
- void [[m]]() {}
+ void [!m!]() {}
}
''');
@@ -142,7 +144,7 @@
}
class B extends A {
- void [[m]]<T>() {}
+ void [!m!]<T>() {}
}
''');
@@ -152,11 +154,11 @@
}
class B extends A {
- void [[b]]() {}
+ void /*[0*/b/*0]*/() {}
}
class C extends A {
- void [[b]]() {}
+ void /*[1*/b/*1]*/() {}
}
''');
@@ -166,20 +168,47 @@
}
class B extends A {
- void [[b]]() {}
+ void /*[0*/b/*0]*/() {}
}
class D extends B {
- void [[b]]() {}
+ void /*[1*/b/*1]*/() {}
}
class E extends B {}
class F extends E {
- void [[b]]() {}
+ void /*[2*/b/*2]*/() {}
}
''');
+ /// Check that implementations that come from mixins in other files return the
+ /// correct location for the implementation.
+ Future<void> test_mixins() async {
+ final mixinsContent = r'''
+import 'main.dart';
+
+mixin MyMixin implements MyInterface {
+ String get [!interfaceField!] => '';
+}
+''';
+ final content = r'''
+import 'other.dart';
+
+class A with MyMixin {}
+class B with MyMixin {}
+class C implements MyInterface {
+ String get [!interfaceField!] => '';
+}
+
+class MyInterface {
+ String get interf^aceField;
+}
+''';
+
+ await _testMarkedContent(content, otherContent: mixinsContent);
+ }
+
Future<void> test_nonDartFile() async {
newFile(pubspecFilePath, simplePubspecContent);
await initialize();
@@ -188,28 +217,47 @@
expect(res, isEmpty);
}
- /// Takes an input string that contains ^ at the location to invoke the
- /// `textDocument/implementations` command and has ranges marked with
- /// `[[brackets]]` that are expected to be included.
+ /// Parses [content] using as [TestCode] and invokes the
+ /// `textDocument/implementations` command at the marked location to verify
+ /// the marked regions are returned as implementations.
+ ///
+ /// If [otherContent] is provided, will be written as `other.dart` alongside
+ /// the file and any marked regions will also be verified.
///
/// If [expectResults] is `false` then expects none of the ranges marked with
- /// `[[brackets]]` to be present in the results instead.
+ /// `/*[0*/brackets/*0]*/` to be present in the results instead.
Future<void> _testMarkedContent(
String content, {
+ String? otherContent,
bool expectResults = true,
}) async {
+ final otherFilePath = join(projectFolderPath, 'lib', 'other.dart');
+ final otherFileUri = Uri.file(otherFilePath);
+ final code = TestCode.parse(content);
+ final otherCode =
+ otherContent != null ? TestCode.parse(otherContent) : null;
+ if (otherCode != null) {
+ newFile(otherFilePath, otherCode.code);
+ }
+
await initialize();
- await openFile(mainFileUri, withoutMarkers(content));
+ await openFile(mainFileUri, code.code);
final res = await getImplementations(
mainFileUri,
- positionFromMarker(content),
+ code.position.position,
);
- final expectedLocations = rangesFromMarkers(content)
- .map((r) => Location(uri: mainFileUri, range: r));
+ final expectedLocations = [
+ for (final range in code.ranges)
+ Location(uri: mainFileUri, range: range.range),
+ if (otherCode != null)
+ for (final range in otherCode.ranges)
+ Location(uri: otherFileUri, range: range.range),
+ ];
if (expectResults) {
+ expect(expectedLocations, isNotEmpty);
expect(res, unorderedEquals(expectedLocations));
} else {
for (final location in expectedLocations) {