| // Copyright (c) 2018, 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.dart'; |
| import 'package:analysis_server/src/analysis_server.dart'; |
| import 'package:analysis_server/src/lsp/client_capabilities.dart'; |
| import 'package:analysis_server/src/lsp/constants.dart'; |
| import 'package:analysis_server/src/lsp/error_or.dart'; |
| import 'package:analysis_server/src/lsp/handlers/handler_cancel_request.dart'; |
| import 'package:analysis_server/src/lsp/handlers/handler_reject.dart'; |
| import 'package:analysis_server/src/lsp/lsp_analysis_server.dart'; |
| import 'package:analysis_server/src/lsp/progress.dart'; |
| import 'package:analysis_server/src/request_handler_mixin.dart'; |
| import 'package:analyzer/dart/analysis/results.dart'; |
| import 'package:analyzer/source/line_info.dart'; |
| import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart'; |
| import 'package:analyzer/src/util/performance/operation_performance.dart'; |
| import 'package:analyzer/src/utilities/cancellation.dart'; |
| import 'package:analyzer_plugin/protocol/protocol.dart'; |
| import 'package:analyzer_plugin/src/protocol/protocol_internal.dart'; |
| import 'package:analyzer_plugin/src/utilities/client_uri_converter.dart'; |
| import 'package:language_server_protocol/json_parsing.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as path; |
| |
| export 'package:analyzer/src/utilities/cancellation.dart'; |
| |
| /// Converts an iterable using the provided function and skipping over any |
| /// null values. |
| Iterable<T> convert<T, E>(Iterable<E> items, T? Function(E) converter) { |
| // TODO(dantup): Now this is used outside of handlers, is there somewhere |
| // better to put it, and/or a better name for it? |
| return items.map(converter).where((item) => item != null).cast<T>(); |
| } |
| |
| abstract class CommandHandler<P, R> with Handler<R>, HandlerHelperMixin { |
| @override |
| final LspAnalysisServer server; |
| |
| CommandHandler(this.server); |
| |
| /// Whether this command records its own analytics and should be excluded from |
| /// logging by the main command handler. |
| /// |
| /// This is useful if a command is generic (for example "performRefactor") and |
| /// can record a more specific command name. |
| bool get recordsOwnAnalytics => false; |
| |
| Future<ErrorOr<Object?>> handle( |
| MessageInfo message, |
| Map<String, Object?> parameters, |
| ProgressReporter progress, |
| CancellationToken cancellationToken, |
| ); |
| } |
| |
| mixin Handler<T> { |
| // TODO(dantup): Merge this into HandlerHelperMixin by converting to methods |
| // so T can be inferred. |
| final fileModifiedError = error<T>( |
| ErrorCodes.ContentModified, |
| 'Document was modified before operation completed', |
| ); |
| |
| final serverNotInitializedError = error<T>( |
| ErrorCodes.ServerNotInitialized, |
| 'Request not valid before server is initialized', |
| ); |
| } |
| |
| /// Provides some helpers for request handlers to produce common errors or |
| /// obtain resolved results after waiting for in-progress analysis. |
| mixin HandlerHelperMixin<S extends AnalysisServer> { |
| path.Context get pathContext => server.resourceProvider.pathContext; |
| |
| S get server; |
| |
| ClientUriConverter get uriConverter => server.uriConverter; |
| |
| /// A [Future] that completes when both the client has finished initializing |
| /// and any in-progress context rebuilds are complete. |
| Future<void> get _initializedWithContexts => Future.value( |
| server.lspInitialized, |
| ).then((_) => server.analysisContextsRebuilt); |
| |
| ErrorOr<T> analysisFailedError<T>(String path) => error<T>( |
| ServerErrorCodes.FileAnalysisFailed, |
| 'Analysis failed for file', |
| path, |
| ); |
| |
| /// Extracts the current document version from [textDocument] if available, |
| /// or uses the version that the server has via |
| /// [LspAnalysisServer.getVersionedDocumentIdentifier]. |
| OptionalVersionedTextDocumentIdentifier extractDocumentVersion( |
| TextDocumentIdentifier textDocument, |
| String path, |
| ) { |
| return switch (textDocument) { |
| OptionalVersionedTextDocumentIdentifier() => textDocument, |
| VersionedTextDocumentIdentifier() => |
| OptionalVersionedTextDocumentIdentifier( |
| uri: textDocument.uri, |
| version: textDocument.version, |
| ), |
| _ => server.getVersionedDocumentIdentifier(path), |
| }; |
| } |
| |
| bool fileHasBeenModified(String path, int? clientVersion) { |
| var serverDocumentVersion = server.getDocumentVersion(path); |
| return clientVersion != null && clientVersion != serverDocumentVersion; |
| } |
| |
| ErrorOr<T> fileNotAnalyzedError<T>(String path) => error<T>( |
| ServerErrorCodes.FileNotAnalyzed, |
| 'File is not being analyzed', |
| path, |
| ); |
| |
| DartdocDirectiveInfo getDartdocDirectiveInfoFor(ResolvedUnitResult result) { |
| return server.getDartdocDirectiveInfoFor(result); |
| } |
| |
| ErrorOr<LineInfo> getLineInfo(String path) { |
| var lineInfo = server.getLineInfo(path); |
| |
| if (lineInfo == null) { |
| return error( |
| ServerErrorCodes.InvalidFilePath, |
| 'Unable to obtain line information for file', |
| path, |
| ); |
| } else { |
| return success(lineInfo); |
| } |
| } |
| |
| /// Returns whether the file at [uri] is a user-editable document. |
| /// |
| /// Only editable documents have overlays and can be modified by the client. |
| bool isEditableDocument(Uri uri) { |
| // Currently, only file:// URIs are editable documents. |
| return uri.isScheme('file'); |
| } |
| |
| /// Returns the file system path (or internal analyzer file reference) for a |
| /// TextDocumentIdentifier. |
| ErrorOr<String> pathOfDoc(TextDocumentIdentifier doc) => pathOfUri(doc.uri); |
| |
| /// Returns the file system path (or internal analyzer file reference) for a |
| /// TextDocumentItem. |
| ErrorOr<String> pathOfDocItem(TextDocumentItem doc) => pathOfUri(doc.uri); |
| |
| /// Returns the file system path (or internal analyzer file reference) for a |
| /// file URI. |
| ErrorOr<String> pathOfUri(Uri? uri) { |
| if (uri == null) { |
| return ErrorOr<String>.error( |
| ResponseError( |
| code: ServerErrorCodes.InvalidFilePath, |
| message: 'Document URI was not supplied', |
| ), |
| ); |
| } |
| |
| // For URIs with no scheme, assume it was a relative path and provide a |
| // better message than "scheme '' is not supported". |
| if (uri.scheme.isEmpty) { |
| return ErrorOr<String>.error( |
| ResponseError( |
| code: ServerErrorCodes.InvalidFilePath, |
| message: 'URI is not a valid file:// URI', |
| data: uri.toString(), |
| ), |
| ); |
| } |
| |
| var supportedSchemes = server.uriConverter.supportedSchemes; |
| var isValidScheme = supportedSchemes.contains(uri.scheme); |
| if (!isValidScheme) { |
| var supportedSchemesString = |
| supportedSchemes.isEmpty |
| ? '(none)' |
| : supportedSchemes.map((scheme) => "'$scheme'").join(', '); |
| return ErrorOr<String>.error( |
| ResponseError( |
| code: ServerErrorCodes.InvalidFilePath, |
| message: |
| "URI scheme '${uri.scheme}' is not supported. " |
| 'Allowed schemes are $supportedSchemesString.', |
| data: uri.toString(), |
| ), |
| ); |
| } |
| try { |
| var context = server.resourceProvider.pathContext; |
| var isWindows = context.style == path.Style.windows; |
| |
| // Use toFilePath() here and not context.fromUri() because they're not |
| // quite the same. `toFilePath()` will throw for some kinds of invalid |
| // file URIs (such as those with fragments) that context.fromUri() does |
| // not. We want to validate using the stricter handling. |
| var filePath = uri |
| .replace(scheme: 'file') // We can only use toFilePath() with file:// |
| .toFilePath(windows: isWindows); |
| |
| // On Windows, paths that start with \ and not a drive letter are not |
| // supported but will return `true` from `path.isAbsolute` so check for them |
| // specifically. |
| if (isWindows && filePath.startsWith(r'\')) { |
| return ErrorOr<String>.error( |
| ResponseError( |
| code: ServerErrorCodes.InvalidFilePath, |
| message: |
| 'URI does not contain an absolute file path ' |
| '(missing drive letter)', |
| data: uri.toString(), |
| ), |
| ); |
| } |
| // Use the proper converter for the return value. |
| return ErrorOr<String>.success(uriConverter.fromClientUri(uri)); |
| } catch (e) { |
| // Even if tryParse() works and file == scheme, fromUri() can throw on |
| // Windows if there are invalid characters. |
| return ErrorOr<String>.error( |
| ResponseError( |
| code: ServerErrorCodes.InvalidFilePath, |
| message: 'URI does not contain a valid file path', |
| data: uri.toString(), |
| ), |
| ); |
| } |
| } |
| |
| /// Attempts to get a [ResolvedLibraryResult] for the library the includes the |
| /// file at [path] or an error. |
| /// |
| /// When [waitForInProgressContextRebuilds] is `true` and the file appears to |
| /// not be analyzed but analysis roots are currently being discovered, will |
| /// wait for discovery to complete and then try again (once) to get a result. |
| Future<ErrorOr<ResolvedLibraryResult>> requireResolvedLibrary( |
| String path, { |
| bool waitForInProgressContextRebuilds = true, |
| }) async { |
| var result = await server.getResolvedLibrary(path); |
| if (result == null) { |
| // Handle retry if allowed. |
| if (waitForInProgressContextRebuilds) { |
| await _initializedWithContexts; |
| return requireResolvedLibrary( |
| path, |
| waitForInProgressContextRebuilds: false, |
| ); |
| } |
| |
| // If the file was being analyzed and we got a null result, that usually |
| // indicates a parser or analysis failure, so provide a more specific |
| // message. |
| return server.isAnalyzed(path) |
| ? analysisFailedError(path) |
| : fileNotAnalyzedError(path); |
| } |
| return success(result); |
| } |
| |
| /// Attempts to get a [ResolvedUnitResult] for [path] or an error. |
| /// |
| /// When [waitForInProgressContextRebuilds] is `true` and the file appears to |
| /// not be analyzed but analysis roots are currently being discovered, will |
| /// wait for discovery to complete and then try again (once) to get a result. |
| Future<ErrorOr<ResolvedUnitResult>> requireResolvedUnit( |
| String path, { |
| bool waitForInProgressContextRebuilds = true, |
| }) async { |
| var result = await server.getResolvedUnit(path); |
| if (result == null) { |
| // Handle retry if allowed. |
| if (waitForInProgressContextRebuilds) { |
| await _initializedWithContexts; |
| return requireResolvedUnit( |
| path, |
| waitForInProgressContextRebuilds: false, |
| ); |
| } |
| |
| // If the file was being analyzed and we got a null result, that usually |
| // indicates a parser or analysis failure, so provide a more specific |
| // message. |
| return server.isAnalyzed(path) |
| ? analysisFailedError(path) |
| : fileNotAnalyzedError(path); |
| } else if (!result.exists) { |
| return error( |
| ServerErrorCodes.InvalidFilePath, |
| 'File does not exist', |
| path, |
| ); |
| } |
| return success(result); |
| } |
| |
| Future<ErrorOr<ParsedUnitResult>> requireUnresolvedUnit( |
| String path, { |
| bool waitForInProgressContextRebuilds = true, |
| }) async { |
| var result = await server.getParsedUnit(path); |
| if (result == null) { |
| // Handle retry if allowed. |
| if (waitForInProgressContextRebuilds) { |
| await _initializedWithContexts; |
| return requireUnresolvedUnit( |
| path, |
| waitForInProgressContextRebuilds: false, |
| ); |
| } |
| |
| // If the file was being analyzed and we got a null result, that usually |
| // indicates a parser or analysis failure, so provide a more specific |
| // message. |
| return server.isAnalyzed(path) |
| ? analysisFailedError(path) |
| : fileNotAnalyzedError(path); |
| } |
| return success(result); |
| } |
| } |
| |
| /// A base class for LSP handlers that require an LSP analysis server and are |
| /// not supported over the legacy protocol. |
| abstract class LspMessageHandler<P, R> |
| extends MessageHandler<P, R, LspAnalysisServer> { |
| LspMessageHandler(super.server); |
| |
| /// All strict LSP handlers implicitly require a trusted handler because they |
| /// either modify state (eg. `textDocument/didOpen`) or otherwise require an |
| /// LSP server (and not a legacy server). |
| @override |
| bool get requiresTrustedCaller => true; |
| } |
| |
| mixin LspPluginRequestHandlerMixin<T extends AnalysisServer> |
| on RequestHandlerMixin<T> { |
| Future<List<Response>> requestFromPlugins( |
| String path, |
| RequestParams params, { |
| Duration timeout = const Duration(milliseconds: 500), |
| }) { |
| var driver = server.getAnalysisDriver(path); |
| var pluginFutures = server.broadcastRequestToPlugins(params, driver); |
| return waitForResponses( |
| pluginFutures, |
| requestParameters: params, |
| timeout: timeout, |
| ); |
| } |
| } |
| |
| /// An object that can handle messages and produce responses for requests. |
| /// |
| /// Clients may not extend, implement or mix-in this class. |
| abstract class MessageHandler<P, R, S extends AnalysisServer> |
| with Handler<R>, HandlerHelperMixin, RequestHandlerMixin<S> { |
| @override |
| final S server; |
| |
| MessageHandler(this.server); |
| |
| /// The method that this handler can handle. |
| Method get handlesMessage; |
| |
| /// Whether this handler is experimental (that is, the method is currently |
| /// prefixed with 'experimental/'). |
| @nonVirtual |
| bool get isExperimental => |
| handlesMessage.toString().startsWith('experimental/'); |
| |
| /// A handler that can parse and validate JSON params. |
| LspJsonHandler<P> get jsonHandler; |
| |
| /// Whether or not this handler can only be called by the owner of the |
| /// analysis server process (for example the editor). |
| /// |
| /// All LSP-only handlers implicitly require a trusted caller because they |
| /// can only be called over the stdin/stdout stream. However, shared message |
| /// handlers must explicitly indicate if they can be called by untrusted |
| /// clients (such as over DTD). |
| /// |
| /// For example, the request to change the DTD connection is _not_ callable |
| /// by a DTD client and only by the editor. |
| bool get requiresTrustedCaller; |
| |
| FutureOr<ErrorOr<R>> handle( |
| P params, |
| MessageInfo message, |
| CancellationToken token, |
| ); |
| |
| /// Handle the given [message]. If the [message] is a [RequestMessage], then the |
| /// return value will be sent back in a [ResponseMessage]. |
| /// [NotificationMessage]s are not expected to return results. |
| FutureOr<ErrorOr<R>> handleMessage( |
| IncomingMessage message, |
| MessageInfo messageInfo, |
| CancellationToken token, |
| ) { |
| var reporter = LspJsonReporter('params'); |
| var paramsJson = message.params as Map<String, Object?>?; |
| if (!jsonHandler.validateParams(paramsJson, reporter)) { |
| return error( |
| ErrorCodes.InvalidParams, |
| 'Invalid params for ${message.method}:\n' |
| '${reporter.errors.isNotEmpty ? reporter.errors.first : ''}' |
| .trim(), |
| ); |
| } |
| |
| var params = |
| paramsJson != null ? jsonHandler.convertParams(paramsJson) : null as P; |
| return handle(params, messageInfo, token); |
| } |
| } |
| |
| /// Additional information about an incoming message (request or notification) |
| /// provided to a handler. |
| class MessageInfo { |
| /// Returns the amount of time (in milliseconds) since the client sent this |
| /// request or `null` if the client did not provide `clientRequestTime`. |
| final int? timeSinceRequest; |
| |
| final OperationPerformanceImpl performance; |
| |
| /// The capabilities for the client that sent the message/request. |
| /// |
| /// For messages from an editor these are the capabilities exchanged during |
| /// initialization, but for requests from other sources (such as DTD) they may |
| /// be a fixed set (or depend on the specific caller) so that these callers do |
| /// not receive different format responses depending on the capabilities of |
| /// the owning editor. |
| /// |
| /// May be null for messages sent prior to initialization. |
| final LspClientCapabilities? clientCapabilities; |
| |
| /// The completer used to indicate that the handler is paused waiting on user |
| /// response. The completer, if any, is set to complete before a dialog is |
| /// shown. |
| final Completer<void>? completer; |
| |
| MessageInfo({ |
| required this.performance, |
| // TODO(dantup): Consider a version of this that has a non-nullable |
| // `LspClientCapabilities` since the majority of handlers first check this |
| // is non-null and reject the request. Only a small number of handlers need |
| // to run without, so it would remove a bunch of boilerplate in the others. |
| required this.clientCapabilities, |
| this.timeSinceRequest, |
| this.completer, |
| }); |
| } |
| |
| mixin PositionalArgCommandHandler { |
| /// Parses "legacy" arguments passed a list, rather than in a map as a single |
| /// argument. |
| /// |
| /// This is provided for backwards compatibility and may not be provided by |
| /// all command handlers. |
| Map<String, Object?> parseArgList(List<Object?> arguments); |
| } |
| |
| /// A message handler that handles all messages for a given server state. |
| abstract class ServerStateMessageHandler { |
| final AnalysisServer server; |
| final Map<Method, MessageHandler<Object?, Object?, AnalysisServer>> |
| messageHandlers = {}; |
| final CancelRequestHandler cancelHandler; |
| final NotCancelableToken _notCancelableToken = NotCancelableToken(); |
| |
| ServerStateMessageHandler(this.server) |
| : cancelHandler = CancelRequestHandler(server) { |
| registerHandler(cancelHandler); |
| } |
| |
| /// Handle the given [message]. If the [message] is a [RequestMessage], then the |
| /// return value will be sent back in a [ResponseMessage]. |
| /// [NotificationMessage]s are not expected to return results. |
| FutureOr<ErrorOr<Object?>> handleMessage( |
| IncomingMessage message, |
| MessageInfo messageInfo, { |
| CancellationToken? cancellationToken, |
| }) async { |
| var handler = messageHandlers[message.method]; |
| if (handler == null) { |
| return handleUnknownMessage(message); |
| } |
| |
| if (message is! RequestMessage) { |
| return handler.handleMessage(message, messageInfo, _notCancelableToken); |
| } |
| |
| // If we weren't provided an existing cancellation token (eg. by the legacy |
| // server), create a new cancellation token that will allow us to cancel |
| // this request if requested. This saves some processing but the handler |
| // will need to specifically check the token after `await`s. |
| cancellationToken ??= cancelHandler.createToken(message); |
| try { |
| if (cancellationToken.isCancellationRequested) { |
| return cancelled(cancellationToken); |
| } |
| var result = await handler.handleMessage( |
| message, |
| messageInfo, |
| cancellationToken, |
| ); |
| // Do a final check before returning the result, because if the request was |
| // cancelled we can save the overhead of serializing everything to JSON |
| // and the client to deserializing the same in order to read the ID to see |
| // that it was a request it didn't need (in the case of completions this |
| // can be quite large). |
| await Future.delayed(Duration.zero); |
| return cancellationToken.isCancellationRequested |
| ? cancelled(cancellationToken) |
| : result; |
| } finally { |
| cancelHandler.clearToken(message); |
| } |
| } |
| |
| FutureOr<ErrorOr<Object?>> handleUnknownMessage(IncomingMessage message) { |
| // If it's an optional *Notification* we can ignore it (return success). |
| // Otherwise respond with failure. Optional Requests must still be responded |
| // to so they don't leave open requests on the client. |
| return _isOptionalNotification(message) |
| ? success(null) |
| : error(ErrorCodes.MethodNotFound, 'Unknown method ${message.method}'); |
| } |
| |
| void registerHandler( |
| MessageHandler<Object?, Object?, AnalysisServer> handler, |
| ) { |
| messageHandlers[handler.handlesMessage] = handler; |
| } |
| |
| void reject(Method method, ErrorCodes code, String message) { |
| registerHandler(RejectMessageHandler(server, method, code, message)); |
| } |
| |
| bool _isOptionalNotification(IncomingMessage message) { |
| // Not a notification. |
| if (message is! NotificationMessage) { |
| return false; |
| } |
| |
| // Messages that start with $/ are optional. |
| var stringValue = message.method.toJson(); |
| return stringValue.startsWith(r'$/'); |
| } |
| } |
| |
| /// A base class for LSP handlers that work with any [AnalysisServer]. |
| abstract class SharedMessageHandler<P, R> |
| extends MessageHandler<P, R, AnalysisServer> { |
| SharedMessageHandler(super.server); |
| } |