mirror of
https://github.com/dart-lang/sdk
synced 2024-09-05 00:13:50 +00:00
[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:
parent
b5846d8aba
commit
6f8d1e4859
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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?>);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
|
|
359
pkg/analysis_server/test/lsp/type_hierarchy_test.dart
Normal file
359
pkg/analysis_server/test/lsp/type_hierarchy_test.dart
Normal 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!);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue