blob: 725ea58343a79e35b6cf55d4b5642c70f0509c73 [file] [log] [blame]
// 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_call_hierarchy.dart'
as call_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 `callHierarchy/incoming` that returns the incoming calls for
/// the target supplied by the client.
class IncomingCallHierarchyHandler extends _AbstractCallHierarchyCallsHandler<
CallHierarchyIncomingCallsParams,
CallHierarchyIncomingCallsResult,
CallHierarchyIncomingCall> with _CallHierarchyUtils {
IncomingCallHierarchyHandler(super.server);
@override
Method get handlesMessage => Method.callHierarchy_incomingCalls;
@override
LspJsonHandler<CallHierarchyIncomingCallsParams> get jsonHandler =>
CallHierarchyIncomingCallsParams.jsonHandler;
/// Fetches incoming calls from a [call_hierarchy.DartCallHierarchyComputer].
///
/// This method is invoked by the superclass which handles similar logic for
/// both incoming and outgoing calls.
@override
Future<List<call_hierarchy.CallHierarchyCalls>> getCalls(
call_hierarchy.DartCallHierarchyComputer computer,
call_hierarchy.CallHierarchyItem target,
) =>
computer.findIncomingCalls(target, server.searchEngine);
/// Handles the request by passing the target item to a shared implementation
/// in the superclass.
@override
Future<ErrorOr<CallHierarchyIncomingCallsResult>> handle(
CallHierarchyIncomingCallsParams params,
MessageInfo message,
CancellationToken token) =>
handleCalls(params.item);
/// Converts a server [call_hierarchy.CallHierarchyCalls] into the correct LSP
/// type for incoming calls.
@override
CallHierarchyIncomingCall toCall(
call_hierarchy.CallHierarchyCalls calls, {
required LineInfo localLineInfo,
required LineInfo itemLineInfo,
required Set<SymbolKind> supportedSymbolKinds,
}) {
return CallHierarchyIncomingCall(
from: toLspItem(
calls.item,
itemLineInfo,
supportedSymbolKinds: supportedSymbolKinds,
),
fromRanges: calls.ranges
// For incoming calls, ranges are in the referenced item so we use
// itemLineInfo and not localLineInfo (which is for the original
// target we're collecting calls to).
.map((call) => sourceRangeToRange(itemLineInfo, call))
.toList(),
);
}
}
/// A handler for `callHierarchy/outgoing` that returns the outgoing calls for
/// the target supplied by the client.
class OutgoingCallHierarchyHandler extends _AbstractCallHierarchyCallsHandler<
CallHierarchyOutgoingCallsParams,
CallHierarchyOutgoingCallsResult,
CallHierarchyOutgoingCall> with _CallHierarchyUtils {
OutgoingCallHierarchyHandler(super.server);
@override
Method get handlesMessage => Method.callHierarchy_outgoingCalls;
@override
LspJsonHandler<CallHierarchyOutgoingCallsParams> get jsonHandler =>
CallHierarchyOutgoingCallsParams.jsonHandler;
/// Fetches outgoing calls from a [call_hierarchy.DartCallHierarchyComputer].
///
/// This method is invoked by the superclass which handles similar logic for
/// both incoming and outgoing calls.
@override
Future<List<call_hierarchy.CallHierarchyCalls>> getCalls(
call_hierarchy.DartCallHierarchyComputer computer,
call_hierarchy.CallHierarchyItem target,
) =>
computer.findOutgoingCalls(target);
/// Handles the request by passing the target item to a shared implementation
/// in the superclass.
@override
Future<ErrorOr<CallHierarchyOutgoingCallsResult>> handle(
CallHierarchyOutgoingCallsParams params,
MessageInfo message,
CancellationToken token) =>
handleCalls(params.item);
/// Converts a server [call_hierarchy.CallHierarchyCalls] into the correct LSP
/// type for outgoing calls.
@override
CallHierarchyOutgoingCall toCall(
call_hierarchy.CallHierarchyCalls calls, {
required LineInfo localLineInfo,
required LineInfo itemLineInfo,
required Set<SymbolKind> supportedSymbolKinds,
}) {
return CallHierarchyOutgoingCall(
to: toLspItem(
calls.item,
itemLineInfo,
supportedSymbolKinds: supportedSymbolKinds,
),
fromRanges: calls.ranges
// For incoming calls, ranges are in original target so we use
// localLineInfo and not itemLineInfo (which is for call target
// the outbound call points to).
.map((call) => sourceRangeToRange(localLineInfo, call))
.toList(),
);
}
}
/// A handler for the initial "prepare" request for starting navigation with
/// Call Hierarchy.
///
/// This handler returns the initial target based on the offset where the
/// feature is invoked. Invocations at call sites will resolve to the respective
/// declarations.
///
/// The target returned by this handler will be sent back to the server for
/// incoming/outgoing calls as the user navigates the call hierarchy in the
/// client.
class PrepareCallHierarchyHandler extends MessageHandler<
CallHierarchyPrepareParams,
TextDocumentPrepareCallHierarchyResult> with _CallHierarchyUtils {
PrepareCallHierarchyHandler(super.server);
@override
Method get handlesMessage => Method.textDocument_prepareCallHierarchy;
@override
LspJsonHandler<CallHierarchyPrepareParams> get jsonHandler =>
CallHierarchyPrepareParams.jsonHandler;
@override
Future<ErrorOr<TextDocumentPrepareCallHierarchyResult>> handle(
CallHierarchyPrepareParams 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 supportedSymbolKinds = clientCapabilities.documentSymbolKinds;
final computer = call_hierarchy.DartCallHierarchyComputer(unit.result);
final target = computer.findTarget(offset);
if (target == null) {
return success(null);
}
return _convertTarget(
unit.result.session,
target,
supportedSymbolKinds,
);
});
}
/// Converts a server [call_hierarchy.CallHierarchyItem] to the LSP protocol
/// equivalent.
Future<ErrorOr<TextDocumentPrepareCallHierarchyResult>> _convertTarget(
AnalysisSession session,
call_hierarchy.CallHierarchyItem target,
Set<SymbolKind> supportedSymbolKinds,
) async {
// Since the target might be in a different file (for example if invoked at
// a call site), we need to get a consistent LineInfo for the target file
// for this session.
final targetFile = session.getFile(target.file);
if (targetFile is! FileResult) {
return error(
ErrorCodes.InternalError,
'Call Hierarchy target was in an unavailable file: '
'${target.displayName} in ${target.file}',
);
}
final targetLineInfo = targetFile.lineInfo;
final item = toLspItem(
target,
targetLineInfo,
supportedSymbolKinds: supportedSymbolKinds,
);
return success([item]);
}
}
/// An abstract base class for incoming and outgoing CallHierarchy handlers
/// which perform largely the same task using different LSP classes.
abstract class _AbstractCallHierarchyCallsHandler<P, R, C>
extends MessageHandler<P, R> with _CallHierarchyUtils {
_AbstractCallHierarchyCallsHandler(super.server);
/// Gets the appropriate types of calls for this handler.
Future<List<call_hierarchy.CallHierarchyCalls>> getCalls(
call_hierarchy.DartCallHierarchyComputer computer,
call_hierarchy.CallHierarchyItem target);
/// Handles a request for incoming or outgoing calls (handled by the concrete
/// implementation) by delegating fetching and converting calls to the
/// subclass.
Future<ErrorOr<List<C>?>> handleCalls(CallHierarchyItem item) async {
if (!isDartUri(item.uri)) {
return success(const []);
}
final clientCapabilities = server.clientCapabilities;
if (clientCapabilities == null) {
// This should not happen unless a client misbehaves.
return failure(serverNotInitializedError);
}
final pos = item.selectionRange.start;
final path = pathOfUri(Uri.parse(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);
// 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',
);
}
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
/// LSP type [C].
C toCall(
call_hierarchy.CallHierarchyCalls calls, {
required LineInfo localLineInfo,
required LineInfo itemLineInfo,
required Set<SymbolKind> supportedSymbolKinds,
});
C? _convertCall(
AnalysisSession session,
LineInfo localLineInfo,
Map<String, LineInfo?> lineInfoCache,
call_hierarchy.CallHierarchyCalls calls,
Set<SymbolKind> supportedSymbolKinds,
) {
final filePath = calls.item.file;
final itemLineInfo = lineInfoCache.putIfAbsent(filePath, () {
final file = session.getFile(filePath);
return file is FileResult ? file.lineInfo : null;
});
if (itemLineInfo == null) {
return null;
}
return toCall(
calls,
localLineInfo: localLineInfo,
itemLineInfo: itemLineInfo,
supportedSymbolKinds: supportedSymbolKinds,
);
}
List<C> _convertCalls(
ResolvedUnitResult unit,
List<call_hierarchy.CallHierarchyCalls> calls,
Set<SymbolKind> supportedSymbolKinds,
) {
final session = unit.session;
final lineInfoCache = <String, LineInfo?>{};
final results = convert(
calls,
(call_hierarchy.CallHierarchyCalls call) => _convertCall(
session,
unit.lineInfo,
lineInfoCache,
call,
supportedSymbolKinds,
),
);
return results.toList();
}
}
/// Utility methods used by all Call Hierarchy handlers.
mixin _CallHierarchyUtils {
/// A mapping from server kinds to LSP [SymbolKind]s.
static const toSymbolKindMapping = {
call_hierarchy.CallHierarchyKind.class_: SymbolKind.Class,
call_hierarchy.CallHierarchyKind.constructor: SymbolKind.Constructor,
call_hierarchy.CallHierarchyKind.extension: SymbolKind.Class,
call_hierarchy.CallHierarchyKind.file: SymbolKind.File,
call_hierarchy.CallHierarchyKind.function: SymbolKind.Function,
call_hierarchy.CallHierarchyKind.method: SymbolKind.Method,
call_hierarchy.CallHierarchyKind.mixin: SymbolKind.Class,
call_hierarchy.CallHierarchyKind.property: SymbolKind.Property,
};
/// A mapping from LSP [SymbolKind]s to server kinds.
static final fromSymbolKindMapping = {
for (final entry in toSymbolKindMapping.entries) entry.value: entry.key,
};
/// Converts a [SymbolKind] passed back from the client over LSP to a server
/// [call_hierarchy.CallHierarchyKind].
call_hierarchy.CallHierarchyKind fromSymbolKind(SymbolKind kind) {
var result = fromSymbolKindMapping[kind];
assert(result != null);
return result ?? call_hierarchy.CallHierarchyKind.unknown;
}
/// Converts a server [SourceRange] to an LSP [Range].
Range sourceRangeToRange(LineInfo lineInfo, SourceRange range) =>
toRange(lineInfo, range.offset, range.length);
/// Converts a server [call_hierarchy.CallHierarchyItem] to an LSP
/// [CallHierarchyItem].
CallHierarchyItem toLspItem(
call_hierarchy.CallHierarchyItem item,
LineInfo lineInfo, {
required Set<SymbolKind> supportedSymbolKinds,
}) {
return CallHierarchyItem(
name: item.displayName,
detail: item.containerName,
kind: toSymbolKind(supportedSymbolKinds, item.kind),
uri: Uri.file(item.file).toString(),
range: sourceRangeToRange(lineInfo, item.codeRange),
selectionRange: sourceRangeToRange(lineInfo, item.nameRange),
);
}
/// Converts an LSP [CallHierarchyItem] supplied by the client back to a
/// server [call_hierarchy.CallHierarchyItem] to use to look up calls.
///
/// 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).
call_hierarchy.CallHierarchyItem? toServerItem(
CallHierarchyItem item,
LineInfo lineInfo, {
required Set<SymbolKind> supportedSymbolKinds,
}) {
final nameRange = toSourceRange(lineInfo, item.selectionRange);
final codeRange = toSourceRange(lineInfo, item.range);
if (nameRange.isError || codeRange.isError) {
return null;
}
return call_hierarchy.CallHierarchyItem(
displayName: item.name,
containerName: item.detail,
kind: fromSymbolKind(item.kind),
file: Uri.parse(item.uri).toFilePath(),
nameRange: nameRange.result,
codeRange: codeRange.result,
);
}
/// Converts a server [call_hierarchy.CallHierarchyKind] to a [SymbolKind]
/// used in the LSP Protocol.
SymbolKind toSymbolKind(Set<SymbolKind> supportedSymbolKinds,
call_hierarchy.CallHierarchyKind kind) {
var result = toSymbolKindMapping[kind];
assert(result != null);
// Handle fallbacks and not-supported kinds.
if (!supportedSymbolKinds.contains(result)) {
if (result == SymbolKind.File) {
result = SymbolKind.Module;
} else {
result = null;
}
}
return result ?? SymbolKind.Obj;
}
}