[analysis_server] Add LSP Type Hierarchy

Fixes https://github.com/Dart-Code/Dart-Code/issues/3313.
Fixes https://github.com/Dart-Code/Dart-Code/issues/2527.

Change-Id: I9f471fd3d7d55999795fee7ab4761e906566bd10
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/264002
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Danny Tuppeny 2022-10-13 19:39:18 +00:00 committed by Commit Queue
parent b5846d8aba
commit 6f8d1e4859
9 changed files with 705 additions and 27 deletions

View file

@ -167,6 +167,13 @@ class TypeHierarchyItem {
/// The range of the code for the declaration of this item.
final SourceRange codeRange;
TypeHierarchyItem({
required this.displayName,
required this.file,
required this.nameRange,
required this.codeRange,
});
TypeHierarchyItem.forElement(Element element)
: displayName = element.displayName,
nameRange = _nameRangeForElement(element),

View file

@ -243,37 +243,33 @@ abstract class _AbstractCallHierarchyCallsHandler<P, R, C>
return failure(serverNotInitializedError);
}
final pos = item.selectionRange.start;
final path = pathOfUri(item.uri);
final unit = await path.mapResult(requireResolvedUnit);
final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
return offset.mapResult((offset) async {
final supportedSymbolKinds = clientCapabilities.documentSymbolKinds;
final computer = call_hierarchy.DartCallHierarchyComputer(unit.result);
final supportedSymbolKinds = clientCapabilities.documentSymbolKinds;
final computer = call_hierarchy.DartCallHierarchyComputer(unit.result);
// Convert the clients item back to one in the servers format so that we
// can use it to get incoming/outgoing calls.
final target = toServerItem(
item,
unit.result.lineInfo,
supportedSymbolKinds: supportedSymbolKinds,
// Convert the clients item back to one in the servers format so that we
// can use it to get incoming/outgoing calls.
final target = toServerItem(
item,
unit.result.lineInfo,
supportedSymbolKinds: supportedSymbolKinds,
);
if (target == null) {
return error(
ErrorCodes.ContentModified,
'Content was modified since Call Hierarchy node was produced',
);
}
if (target == null) {
return error(
ErrorCodes.ContentModified,
'Content was modified since Call Hierarchy node was produced',
);
}
final calls = await getCalls(computer, target);
final results = _convertCalls(
unit.result,
calls,
supportedSymbolKinds,
);
return success(results);
});
final calls = await getCalls(computer, target);
final results = _convertCalls(
unit.result,
calls,
supportedSymbolKinds,
);
return success(results);
}
/// Converts a server [call_hierarchy.CallHierarchyCalls] to the appropriate

View file

@ -38,6 +38,7 @@ import 'package:analysis_server/src/lsp/handlers/handler_shutdown.dart';
import 'package:analysis_server/src/lsp/handlers/handler_signature_help.dart';
import 'package:analysis_server/src/lsp/handlers/handler_text_document_changes.dart';
import 'package:analysis_server/src/lsp/handlers/handler_type_definition.dart';
import 'package:analysis_server/src/lsp/handlers/handler_type_hierarchy.dart';
import 'package:analysis_server/src/lsp/handlers/handler_will_rename_files.dart';
import 'package:analysis_server/src/lsp/handlers/handler_workspace_configuration.dart';
import 'package:analysis_server/src/lsp/handlers/handler_workspace_symbols.dart';
@ -102,6 +103,9 @@ class InitializedStateMessageHandler extends ServerStateMessageHandler {
registerHandler(PrepareCallHierarchyHandler(server));
registerHandler(IncomingCallHierarchyHandler(server));
registerHandler(OutgoingCallHierarchyHandler(server));
registerHandler(PrepareTypeHierarchyHandler(server));
registerHandler(TypeHierarchySubtypesHandler(server));
registerHandler(TypeHierarchySupertypesHandler(server));
registerHandler(FoldingHandler(server));
registerHandler(DiagnosticServerHandler(server));
registerHandler(WorkspaceSymbolHandler(server));

View file

@ -0,0 +1,260 @@
// Copyright (c) 2022, 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 'dart:async';
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/src/computer/computer_lazy_type_hierarchy.dart'
as type_hierarchy;
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/source/source_range.dart';
/// A handler for the initial "prepare" request for starting navigation with
/// Type Hierarchy.
///
/// This handler returns the initial target based on the offset where the
/// feature is invoked. Invocations at item sites will resolve to the respective
/// declarations.
///
/// The target returned by this handler will be sent back to the server for
/// supertype/supertype items as the user navigates the type hierarchy in the
/// client.
class PrepareTypeHierarchyHandler extends MessageHandler<
TypeHierarchyPrepareParams,
TextDocumentPrepareTypeHierarchyResult> with _TypeHierarchyUtils {
PrepareTypeHierarchyHandler(super.server);
@override
Method get handlesMessage => Method.textDocument_prepareTypeHierarchy;
@override
LspJsonHandler<TypeHierarchyPrepareParams> get jsonHandler =>
TypeHierarchyPrepareParams.jsonHandler;
@override
Future<ErrorOr<TextDocumentPrepareTypeHierarchyResult>> handle(
TypeHierarchyPrepareParams params,
MessageInfo message,
CancellationToken token) async {
if (!isDartDocument(params.textDocument)) {
return success(const []);
}
final clientCapabilities = server.clientCapabilities;
if (clientCapabilities == null) {
// This should not happen unless a client misbehaves.
return serverNotInitializedError;
}
final pos = params.position;
final path = pathOfDoc(params.textDocument);
final unit = await path.mapResult(requireResolvedUnit);
final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
return offset.mapResult((offset) {
final computer =
type_hierarchy.DartLazyTypeHierarchyComputer(unit.result);
final target = computer.findTarget(offset);
if (target == null) {
return success(null);
}
final item = toLspItem(target, unit.result.lineInfo);
return success([item]);
});
}
}
class TypeHierarchySubtypesHandler extends MessageHandler<
TypeHierarchySubtypesParams,
TypeHierarchySubtypesResult> with _TypeHierarchyUtils {
TypeHierarchySubtypesHandler(super.server);
@override
Method get handlesMessage => Method.typeHierarchy_subtypes;
@override
LspJsonHandler<TypeHierarchySubtypesParams> get jsonHandler =>
TypeHierarchySubtypesParams.jsonHandler;
@override
Future<ErrorOr<TypeHierarchySubtypesResult>> handle(
TypeHierarchySubtypesParams params,
MessageInfo message,
CancellationToken token) async {
final item = params.item;
final path = pathOfUri(item.uri);
final unit = await path.mapResult(requireResolvedUnit);
final computer = type_hierarchy.DartLazyTypeHierarchyComputer(unit.result);
// Convert the clients item back to one in the servers format so that we
// can use it to get sub/super types.
final target = toServerItem(item, unit.result.lineInfo);
if (target == null) {
return error(
ErrorCodes.ContentModified,
'Content was modified since Type Hierarchy node was produced',
);
}
final calls = await computer.findSubtypes(target, server.searchEngine);
final results = calls != null ? _convertItems(unit.result, calls) : null;
return success(results);
}
}
class TypeHierarchySupertypesHandler extends MessageHandler<
TypeHierarchySupertypesParams,
TypeHierarchySupertypesResult> with _TypeHierarchyUtils {
TypeHierarchySupertypesHandler(super.server);
@override
Method get handlesMessage => Method.typeHierarchy_supertypes;
@override
LspJsonHandler<TypeHierarchySupertypesParams> get jsonHandler =>
TypeHierarchySupertypesParams.jsonHandler;
@override
Future<ErrorOr<TypeHierarchySupertypesResult>> handle(
TypeHierarchySupertypesParams params,
MessageInfo message,
CancellationToken token) async {
final item = params.item;
final path = pathOfUri(item.uri);
final unit = await path.mapResult(requireResolvedUnit);
final computer = type_hierarchy.DartLazyTypeHierarchyComputer(unit.result);
// Convert the clients item back to one in the servers format so that we
// can use it to get sub/super types.
final target = toServerItem(item, unit.result.lineInfo);
if (target == null) {
return error(
ErrorCodes.ContentModified,
'Content was modified since Type Hierarchy node was produced',
);
}
final calls = await computer.findSupertypes(target);
final results = calls != null ? _convertItems(unit.result, calls) : null;
return success(results);
}
}
/// Utility methods used by all Type Hierarchy handlers.
mixin _TypeHierarchyUtils {
/// Converts a server [SourceRange] to an LSP [Range].
Range sourceRangeToRange(LineInfo lineInfo, SourceRange range) =>
toRange(lineInfo, range.offset, range.length);
/// Converts a server [type_hierarchy.TypeHierarchyItem] to an LSP
/// [TypeHierarchyItem].
TypeHierarchyItem toLspItem(
type_hierarchy.TypeHierarchyItem item,
LineInfo lineInfo,
) {
return TypeHierarchyItem(
name: item.displayName,
detail: _detailFor(item),
kind: SymbolKind.Class,
uri: Uri.file(item.file),
range: sourceRangeToRange(lineInfo, item.codeRange),
selectionRange: sourceRangeToRange(lineInfo, item.nameRange),
);
}
/// Converts an LSP [TypeHierarchyItem] supplied by the client back to a
/// server [type_hierarchy.TypeHierarchyItem] to use to look up items.
///
/// Returns `null` if the supplied item is no longer valid (for example its
/// ranges are no longer valid in the current state of the document).
type_hierarchy.TypeHierarchyItem? toServerItem(
TypeHierarchyItem item,
LineInfo lineInfo,
) {
final nameRange = toSourceRange(lineInfo, item.selectionRange);
final codeRange = toSourceRange(lineInfo, item.range);
if (nameRange.isError || codeRange.isError) {
return null;
}
return type_hierarchy.TypeHierarchyItem(
displayName: item.name,
file: item.uri.toFilePath(),
nameRange: nameRange.result,
codeRange: codeRange.result,
);
}
/// Converts a server [type_hierarchy.TypeHierarchyItem] to an LSP
/// [TypeHierarchyItem].
///
/// Reads [LineInfo]s from [session], using [lineInfoCache] as a cache.
TypeHierarchyItem? _convertItem(
AnalysisSession session,
Map<String, LineInfo?> lineInfoCache,
type_hierarchy.TypeHierarchyItem item,
) {
final filePath = item.file;
final lineInfo = lineInfoCache.putIfAbsent(filePath, () {
final file = session.getFile(filePath);
return file is FileResult ? file.lineInfo : null;
});
if (lineInfo == null) {
return null;
}
return toLspItem(item, lineInfo);
}
/// Converts multiple server [type_hierarchy.TypeHierarchyItem] to an LSP
/// [TypeHierarchyItem].
///
/// Reads [LineInfo]s from [unit.session], caching them for items in the same
/// file.
List<TypeHierarchyItem> _convertItems(
ResolvedUnitResult unit,
List<type_hierarchy.TypeHierarchyRelatedItem> items,
) {
final session = unit.session;
final lineInfoCache = <String, LineInfo?>{
unit.path: unit.lineInfo,
};
final results = convert(
items,
(type_hierarchy.TypeHierarchyRelatedItem item) => _convertItem(
session,
lineInfoCache,
item,
),
);
return results.toList();
}
/// Gets the "detail" label for [item].
///
/// This includes a user-visible description of the relationship between the
/// target item and [item].
String? _detailFor(type_hierarchy.TypeHierarchyItem item) {
if (item is! type_hierarchy.TypeHierarchyRelatedItem) {
return null;
}
switch (item.relationship) {
case type_hierarchy.TypeHierarchyItemRelationship.extends_:
return 'extends';
case type_hierarchy.TypeHierarchyItemRelationship.implements:
return 'implements';
case type_hierarchy.TypeHierarchyItemRelationship.mixesIn:
return 'mixes in';
case type_hierarchy.TypeHierarchyItemRelationship.constrainedTo:
return 'constrained to';
default:
return null;
}
}
}

View file

@ -41,6 +41,7 @@ class ClientDynamicRegistrations {
Method.textDocument_selectionRange,
Method.textDocument_typeDefinition,
Method.textDocument_prepareCallHierarchy,
Method.textDocument_prepareTypeHierarchy,
// workspace.fileOperations covers all file operation methods but we only
// support this one.
Method.workspace_willRenameFiles,
@ -123,6 +124,9 @@ class ClientDynamicRegistrations {
bool get typeFormatting =>
_capabilities.textDocument?.onTypeFormatting?.dynamicRegistration ??
false;
bool get typeHierarchy =>
_capabilities.textDocument?.typeHierarchy?.dynamicRegistration ?? false;
}
class ServerCapabilitiesComputer {
@ -296,6 +300,10 @@ class ServerCapabilitiesComputer {
range: Either2<bool, SemanticTokensOptionsRange>.t1(true),
),
),
typeHierarchyProvider: dynamicRegistrations.typeHierarchy
? null
: Either3<bool, TypeHierarchyOptions,
TypeHierarchyRegistrationOptions>.t1(true),
executeCommandProvider: ExecuteCommandOptions(
commands: Commands.serverSupportedCommands,
workDoneProgress: true,
@ -546,6 +554,13 @@ class ServerCapabilitiesComputer {
range: Either2<bool, SemanticTokensOptionsRange>.t1(true),
),
);
register(
dynamicRegistrations.typeHierarchy,
Method.textDocument_prepareTypeHierarchy,
TypeHierarchyRegistrationOptions(
documentSelector: [dartFiles],
),
);
register(
dynamicRegistrations.inlayHints,
Method.textDocument_inlayHint,

View file

@ -41,7 +41,9 @@ class InitializationTest extends AbstractLspAnalysisServerTest {
) {
final options = registrationFor(registrations, method)?.registerOptions;
if (options == null) {
throw 'Registration options for $method were not found';
throw 'Registration options for $method were not found. '
'Perhaps dynamicRegistration is missing from '
'withAllSupportedTextDocumentDynamicRegistrations?';
}
return TextDocumentRegistrationOptions.fromJson(
options as Map<String, Object?>);

View file

@ -325,6 +325,7 @@ mixin ClientCapabilitiesHelperMixin {
tokenModifiers: [],
tokenTypes: []).toJson(),
'typeDefinition': {'dynamicRegistration': true},
'typeHierarchy': {'dynamicRegistration': true},
});
}
@ -1718,6 +1719,18 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
return expectSuccessfulResponseTo(request, PlaceholderAndRange.fromJson);
}
Future<List<TypeHierarchyItem>?> prepareTypeHierarchy(Uri uri, Position pos) {
final request = makeRequest(
Method.textDocument_prepareTypeHierarchy,
TypeHierarchyPrepareParams(
textDocument: TextDocumentIdentifier(uri: uri),
position: pos,
),
);
return expectSuccessfulResponseTo(
request, _fromJsonList(TypeHierarchyItem.fromJson));
}
/// Calls the supplied function and responds to any `workspace/configuration`
/// request with the supplied config.
Future<T> provideConfig<T>(
@ -1932,6 +1945,26 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
);
}
Future<List<TypeHierarchyItem>?> typeHierarchySubtypes(
TypeHierarchyItem item) {
final request = makeRequest(
Method.typeHierarchy_subtypes,
TypeHierarchySubtypesParams(item: item),
);
return expectSuccessfulResponseTo(
request, _fromJsonList(TypeHierarchyItem.fromJson));
}
Future<List<TypeHierarchyItem>?> typeHierarchySupertypes(
TypeHierarchyItem item) {
final request = makeRequest(
Method.typeHierarchy_supertypes,
TypeHierarchySupertypesParams(item: item),
);
return expectSuccessfulResponseTo(
request, _fromJsonList(TypeHierarchyItem.fromJson));
}
/// Tells the server the config has changed, and provides the supplied config
/// when it requests the updated config.
Future<ResponseMessage> updateConfig(Map<String, dynamic> config) {

View file

@ -46,6 +46,7 @@ import 'signature_help_test.dart' as signature_help;
import 'snippets_test.dart' as snippets;
import 'super_test.dart' as get_super;
import 'type_definition_test.dart' as type_definition;
import 'type_hierarchy_test.dart' as type_hierarchy;
import 'will_rename_files_test.dart' as will_rename_files;
import 'workspace_symbols_test.dart' as workspace_symbols;
@ -93,6 +94,7 @@ void main() {
signature_help.main();
snippets.main();
type_definition.main();
type_hierarchy.main();
will_rename_files.main();
workspace_symbols.main();
}, name: 'lsp');

View file

@ -0,0 +1,359 @@
// Copyright (c) 2022, 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:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:collection/collection.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../utils/test_code_format.dart';
import 'server_abstract.dart';
void main() {
defineReflectiveSuite(() {
// These tests cover the LSP handler. A complete set of Type Hierarchy tests
// are in 'test/src/computer/type_hierarchy_computer_test.dart'.
defineReflectiveTests(PrepareTypeHierarchyTest);
defineReflectiveTests(TypeHierarchySupertypesTest);
defineReflectiveTests(TypeHierarchySubtypesTest);
});
}
abstract class AbstractTypeHierarchyTest extends AbstractLspAnalysisServerTest {
/// Code being tested in the main file.
late TestCode code;
/// Another file for testing cross-file content.
late final String otherFilePath;
late final Uri otherFileUri;
late TestCode otherCode;
/// The result of the last prepareTypeHierarchy call.
TypeHierarchyItem? prepareResult;
late final dartCodeUri = Uri.file(convertPath('/sdk/lib/core/core.dart'));
/// Matches a [TypeHierarchyItem] for [Object] with an 'extends' relationship.
Matcher get _isExtendsObject => TypeMatcher<TypeHierarchyItem>()
.having((e) => e.name, 'name', 'Object')
.having((e) => e.uri, 'uri', dartCodeUri)
.having((e) => e.kind, 'kind', SymbolKind.Class)
.having((e) => e.detail, 'detail', 'extends')
.having((e) => e.selectionRange, 'selectionRange', _isValidRange)
.having((e) => e.range, 'range', _isValidRange);
/// Matches a valid [Position].
Matcher get _isValidPosition => TypeMatcher<Position>()
.having((e) => e.line, 'line', greaterThanOrEqualTo(0))
.having((e) => e.character, 'character', greaterThanOrEqualTo(0));
/// Matches a [Range] with valid [Position]s.
Matcher get _isValidRange => TypeMatcher<Range>()
.having((e) => e.start, 'start', _isValidPosition)
.having((e) => e.end, 'end', _isValidPosition);
@override
void setUp() {
super.setUp();
otherFilePath = join(projectFolderPath, 'lib', 'other.dart');
otherFileUri = Uri.file(otherFilePath);
}
/// Matches a [TypeHierarchyItem] with the given values.
Matcher _isItem(
String name,
Uri uri, {
String? detail,
required Range selectionRange,
required Range range,
}) =>
TypeMatcher<TypeHierarchyItem>()
.having((e) => e.name, 'name', name)
.having((e) => e.uri, 'uri', uri)
.having((e) => e.kind, 'kind', SymbolKind.Class)
.having((e) => e.detail, 'detail', detail)
.having((e) => e.selectionRange, 'selectionRange', selectionRange)
.having((e) => e.range, 'range', range);
/// Parses [content] and calls 'textDocument/prepareTypeHierarchy' at the
/// marked location.
Future<void> _prepareTypeHierarchy(String content,
{String? otherContent}) async {
code = TestCode.parse(content);
newFile(mainFilePath, code.code);
if (otherContent != null) {
otherCode = TestCode.parse(otherContent);
newFile(otherFilePath, otherCode.code);
}
await initialize();
final result = await prepareTypeHierarchy(
mainFileUri,
code.position.lsp,
);
prepareResult = result?.singleOrNull;
}
}
@reflectiveTest
class PrepareTypeHierarchyTest extends AbstractTypeHierarchyTest {
Future<void> test_class() async {
final content = '''
/*[0*/class /*[1*/MyC^lass1/*1]*/ {}/*0]*/
''';
await _prepareTypeHierarchy(content);
expect(
prepareResult,
_isItem(
'MyClass1',
mainFileUri,
range: code.ranges[0].lsp,
selectionRange: code.ranges[1].lsp,
),
);
}
Future<void> test_nonClass() async {
final content = '''
int? a^a;
''';
await _prepareTypeHierarchy(content);
expect(prepareResult, isNull);
}
Future<void> test_whitespace() async {
final content = '''
int? a;
^
int? b;
''';
await _prepareTypeHierarchy(content);
expect(prepareResult, isNull);
}
}
@reflectiveTest
class TypeHierarchySubtypesTest extends AbstractTypeHierarchyTest {
List<TypeHierarchyItem>? subtypes;
Future<void> test_anotherFile() async {
final content = '''
class MyCl^ass1 {}
''';
final otherContent = '''
import 'main.dart';
/*[0*/class /*[1*/MyClass2/*1]*/ extends MyClass1 {}/*0]*/
''';
await _fetchSubtypes(content, otherContent: otherContent);
expect(
subtypes,
equals([
_isItem(
'MyClass2',
otherFileUri,
detail: 'extends',
range: otherCode.ranges[0].lsp,
selectionRange: otherCode.ranges[1].lsp,
),
]));
}
Future<void> test_extends() async {
final content = '''
class MyCla^ss1 {}
/*[0*/class /*[1*/MyClass2/*1]*/ extends MyClass1 {}/*0]*/
''';
await _fetchSubtypes(content);
expect(
subtypes,
equals([
_isItem(
'MyClass2',
mainFileUri,
detail: 'extends',
range: code.ranges[0].lsp,
selectionRange: code.ranges[1].lsp,
),
]));
}
Future<void> test_implements() async {
final content = '''
class MyCla^ss1 {}
/*[0*/class /*[1*/MyClass2/*1]*/ implements MyClass1 {}/*0]*/
''';
await _fetchSubtypes(content);
expect(
subtypes,
equals([
_isItem(
'MyClass2',
mainFileUri,
detail: 'implements',
range: code.ranges[0].lsp,
selectionRange: code.ranges[1].lsp,
),
]));
}
Future<void> test_on() async {
final content = '''
class MyCla^ss1 {}
/*[0*/mixin /*[1*/MyMixin1/*1]*/ on MyClass1 {}/*0]*/
''';
await _fetchSubtypes(content);
expect(
subtypes,
equals([
_isItem(
'MyMixin1',
mainFileUri,
detail: 'constrained to',
range: code.ranges[0].lsp,
selectionRange: code.ranges[1].lsp,
),
]));
}
Future<void> test_with() async {
final content = '''
mixin MyMi^xin1 {}
/*[0*/class /*[1*/MyClass1/*1]*/ with MyMixin1 {}/*0]*/
''';
await _fetchSubtypes(content);
expect(
subtypes,
equals([
_isItem(
'MyClass1',
mainFileUri,
detail: 'mixes in',
range: code.ranges[0].lsp,
selectionRange: code.ranges[1].lsp,
),
]));
}
/// Parses [content], calls 'textDocument/prepareTypeHierarchy' at the
/// marked location and then calls 'typeHierarchy/subtypes' with the result.
Future<void> _fetchSubtypes(String content, {String? otherContent}) async {
await _prepareTypeHierarchy(content, otherContent: otherContent);
subtypes = await typeHierarchySubtypes(prepareResult!);
}
}
@reflectiveTest
class TypeHierarchySupertypesTest extends AbstractTypeHierarchyTest {
List<TypeHierarchyItem>? supertypes;
Future<void> test_anotherFile() async {
final content = '''
import 'other.dart';
class MyCla^ss2 extends MyClass1 {}
''';
final otherContent = '''
/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/
''';
await _fetchSupertypes(content, otherContent: otherContent);
expect(
supertypes,
equals([
_isItem(
'MyClass1',
otherFileUri,
detail: 'extends',
range: otherCode.ranges[0].lsp,
selectionRange: otherCode.ranges[1].lsp,
),
]));
}
Future<void> test_extends() async {
final content = '''
/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/
class MyCla^ss2 extends MyClass1 {}
''';
await _fetchSupertypes(content);
expect(
supertypes,
equals([
_isItem(
'MyClass1',
mainFileUri,
detail: 'extends',
range: code.ranges[0].lsp,
selectionRange: code.ranges[1].lsp,
),
]));
}
Future<void> test_implements() async {
final content = '''
/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/
class MyCla^ss2 implements MyClass1 {}
''';
await _fetchSupertypes(content);
expect(
supertypes,
equals([
_isExtendsObject,
_isItem(
'MyClass1',
mainFileUri,
detail: 'implements',
range: code.ranges[0].lsp,
selectionRange: code.ranges[1].lsp,
),
]));
}
Future<void> test_on() async {
final content = '''
/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/
mixin MyMix^in1 on MyClass1 {}
''';
await _fetchSupertypes(content);
expect(
supertypes,
equals([
_isItem(
'MyClass1',
mainFileUri,
detail: 'constrained to',
range: code.ranges[0].lsp,
selectionRange: code.ranges[1].lsp,
),
]));
}
Future<void> test_with() async {
final content = '''
/*[0*/mixin /*[1*/MyMixin1/*1]*/ {}/*0]*/
class MyCla^ss1 with MyMixin1 {}
''';
await _fetchSupertypes(content);
expect(
supertypes,
equals([
_isExtendsObject,
_isItem(
'MyMixin1',
mainFileUri,
detail: 'mixes in',
range: code.ranges[0].lsp,
selectionRange: code.ranges[1].lsp,
),
]));
}
/// Parses [content], calls 'textDocument/prepareTypeHierarchy' at the
/// marked location and then calls 'typeHierarchy/supertypes' with the result.
Future<void> _fetchSupertypes(String content, {String? otherContent}) async {
await _prepareTypeHierarchy(content, otherContent: otherContent);
supertypes = await typeHierarchySupertypes(prepareResult!);
}
}