blob: fab1973bd54f263a4c3d64627141fbad6d1b6f02 [file] [edit]
// Copyright (c) 2026, 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 'dart:convert';
import 'dart:io';
import 'package:analysis_server/src/lsp/constants.dart' show CustomMethods;
import 'package:analysis_server/src/lsp/lsp_packet_transformer.dart';
import 'package:analysis_server/src/server/driver.dart' show Driver;
import 'package:args/args.dart';
import 'package:language_server_protocol/protocol_custom_generated.dart';
import 'package:language_server_protocol/protocol_generated.dart';
import 'package:language_server_protocol/protocol_special.dart';
import 'package:path/path.dart' as path;
import 'core.dart';
import 'experiments.dart';
import 'sdk.dart';
typedef ProcessFactory =
Future<Process> Function(
String executable,
List<String> arguments,
);
typedef _Id = Either2<int, String>;
/// A class to provide an API wrapper around an LSP analysis server process.
class LspAnalysisServer {
LspAnalysisServer(
this._packagesFile,
this._sdkPath,
this._analysisRoots, {
this.cacheDirectoryPath,
required this.commandName,
required this.argResults,
required this._usePlugins,
this.enabledExperiments = const [],
this.suppressAnalytics = false,
this._useAotSnapshot = false,
ProcessFactory? processFactory,
}) : _processFactory =
processFactory ??
((executable, arguments) => Process.start(executable, arguments));
final ProcessFactory _processFactory;
final String? cacheDirectoryPath;
final File? _packagesFile;
final Directory _sdkPath;
final List<FileSystemEntity> _analysisRoots;
final String commandName;
final ArgResults? argResults;
final List<String> enabledExperiments;
final bool suppressAnalytics;
final bool _useAotSnapshot;
final bool _usePlugins;
Process? _process;
final StreamController<String> _outgoingMessages = StreamController<String>(
sync: true,
);
int _id = 0;
bool _shutdownResponseReceived = false;
bool _serverErrorReceived = false;
/// Whether any server error occurred that could mean analysis was not
/// performed correctly.
bool get serverErrorReceived => _serverErrorReceived;
/// Waits for any in-progress initialization or analysis to complete.
///
/// If the server shuts down unexpectedly, the returned future will complete
/// with an error.
Future<void> workspaceAnalysisComplete() {
return _sendRequest(CustomMethods.workspaceAnalysisComplete);
}
Stream<PublishDiagnosticsParams> get onErrors {
return _stream(
Method.textDocument_publishDiagnostics,
PublishDiagnosticsParams.fromJson,
);
}
Future<int> _onExit = Future.value(0);
/// A future that completes when the last spawned process exits with its exit
/// code.
///
/// If no process has been spawned, will return a completed Future with the
/// value 0.
Future<int> get onExit => _onExit;
final Map<Method, StreamController<NotificationMessage>> _streamControllers =
{};
/// Whether the process has crashed.
bool get hasCrashed => _onCrash.isCompleted;
/// Completes when an analysis server crash has been detected.
Future<void> get onCrash => _onCrash.future;
final _onCrash = Completer<void>();
/// Completers that will resolve with the responses for outbound
/// client-to-server requests.
final Map<_Id, Completer<ResponseMessage>> _requestCompleters = {};
/// Starts the process, performs LSP initialization and returns the pid for it.
Future<int> start({bool setAnalysisRoots = true}) async {
if (_process != null) {
throw StateError('Server process is already running');
}
final process = await _startProcess();
_process = process;
_onExit = process.exitCode;
_outgoingMessages.stream
.transform(LspPacketEncoder())
.listen(process.stdin.add);
_shutdownResponseReceived = false;
// This callback hookup can't throw.
process.exitCode.whenComplete(() {
_process = null;
if (!_shutdownResponseReceived) {
// The process exited unexpectedly. Report the crash.
// If `server.error` reported an error, that has been logged by
// `_handleServerError`.
final error = StateError('The analysis server crashed unexpectedly');
// Complete these completers in order to unstick the process.
for (final completer in _requestCompleters.values) {
completer.completeError(error);
}
_onCrash.complete();
}
});
final errorStream = process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter());
errorStream.listen(log.stderr);
final inStream = process.stdout.transform(LspPacketTransformer());
inStream.listen(_handleServerMessage);
_stream(
Method.window_logMessage,
LogMessageParams.fromJson,
).listen(_handleLogMessage);
_stream(
Method.window_showMessage,
ShowMessageParams.fromJson,
).listen(_handleShowMessage);
// The call to `absolute.resolveSymbolicLinksSync()` canonicalizes the path
// to be passed to the analysis server.
List<WorkspaceFolder> workspaceFolders = setAnalysisRoots
? _analysisRoots
.map((root) => root.absolute.resolveSymbolicLinksSync())
.map(
(rootPath) => WorkspaceFolder(
name: path.basename(rootPath),
uri: Uri.file(
rootPath,
),
),
)
.toList()
: [];
var initializeResult = await _expectSuccessfulResponse(
Method.initialize,
InitializeParams(
capabilities: ClientCapabilities(
textDocument: TextDocumentClientCapabilities(
publishDiagnostics: PublishDiagnosticsClientCapabilities(
// Allows us to get URLs in diagnostic codes.
codeDescriptionSupport: true,
),
),
experimental: {
// Request the server includes fields like offset, length, type,
// correctionMessage in Diagnostic.data. These fields are used in
// the machine/JSON outputs of 'dart analyze' and we want to avoid
// breaking potential consumers.
'includeAdditionalDiagnosticData': true,
},
),
workspaceFolders: workspaceFolders,
),
InitializeResult.fromJson,
);
var serverCapabilities = initializeResult.capabilities;
var experimentalCapabilities = serverCapabilities.experimental;
assert(experimentalCapabilities is Map<String, Object?>);
assert(
(experimentalCapabilities as Map<String, Object?>).containsKey(
'workspaceAnalysisComplete',
),
);
_sendNotification(
Method.initialized,
InitializedParams(),
);
return process.pid;
}
Future<Process> _startProcess() {
final executable = _useAotSnapshot ? sdk.dartAotRuntime : sdk.dart;
final arguments = [
if (_useAotSnapshot)
sdk.analysisServerAotSnapshot
else
sdk.analysisServerSnapshot,
if (suppressAnalytics) '--${Driver.suppressAnalyticsFlag}',
'--${Driver.clientIdOption}=dart-$commandName',
'--disable-server-feature-completion',
'--disable-server-feature-search',
'--disable-silent-analysis-exceptions',
'--sdk',
_sdkPath.path,
if (cacheDirectoryPath != null) '--cache=$cacheDirectoryPath',
if (_packagesFile != null) '--packages=${_packagesFile.path}',
if (enabledExperiments.isNotEmpty)
'--$experimentFlagName=${enabledExperiments.join(',')}',
if (!_usePlugins) '--no-plugins',
'--protocol=lsp',
];
log.trace('$executable ${arguments.join(' ')}');
return _processFactory(executable, arguments);
}
/// Sends a hover request.
Future<Hover?> getHover(Uri uri, Position pos) {
return _expectSuccessfulResponse(
Method.textDocument_hover,
TextDocumentPositionParams(
textDocument: TextDocumentIdentifier(uri: uri),
position: pos,
),
Hover.fromJson,
);
}
/// Sends a migrate request.
Future<DartMigrateResult?> migrate(
List<Uri> uris, {
bool? apply,
List<MigrationStep>? steps,
}) {
return _expectSuccessfulResponse(
CustomMethods.migrate,
DartMigrateParams(
uris: uris,
apply: apply,
steps: steps,
),
DartMigrateResult.fromJson,
);
}
Future<void> shutdown({Duration? timeout}) async {
// Request shutdown.
var future = _gracefulShutdown();
if (timeout != null) {
future = future.timeout(
timeout,
onTimeout: () {
log.stderr('The analysis server timed out while shutting down.');
},
);
}
await future.whenComplete(dispose);
}
/// Attempt a graceful shutdown and wait for the process to exit.
Future<void> _gracefulShutdown() async {
try {
await _sendRequest(Method.shutdown);
_shutdownResponseReceived = true;
} catch (_) {}
try {
_sendNotification(Method.exit);
} catch (_) {}
await _process?.exitCode;
}
void _sendLsp(Message message) {
final json = jsonEncode(message.toJson());
log.trace('==> $json');
_outgoingMessages.add(json);
}
void _sendNotification(Method method, [Object? params]) {
final message = NotificationMessage(
method: method,
params: params is ToJsonable ? params.toJson() : params,
jsonrpc: jsonRpcVersion,
);
_sendLsp(message);
}
Future<ResponseMessage> _sendRequest(Method method, [Object? params]) {
final id = _Id.t1(++_id);
final message = RequestMessage(
id: id,
method: method,
params: params is ToJsonable ? params.toJson() : params,
jsonrpc: jsonRpcVersion,
);
final completer = _requestCompleters[id] = Completer<ResponseMessage>();
_sendLsp(message);
return completer.future;
}
void _handleLogMessage(LogMessageParams message) {
if (message.type == MessageType.Error) {
_handleServerError(message.message);
}
}
void _handleServerMessage(String line) {
log.trace('<== $line');
final response = json.decode(line) as Object?;
if (response is Map<String, Object?>) {
var message = Message.fromJson(response);
switch (message) {
case NotificationMessage():
_streamControllers[message.method]?.add(message);
break;
case RequestMessage():
// We don't expect reverse-requests from the server in this mode.
break;
case ResponseMessage():
_requestCompleters.remove(message.id)?.complete(message);
break;
}
}
}
void _handleServerError(String message, [String? stackTrace]) {
_serverErrorReceived = true;
log.stderr('An unexpected error was encountered by the Analysis Server.');
log.stderr(
'Please file an issue at '
'https://github.com/dart-lang/sdk/issues/new/choose with the following '
'details:\n',
);
log.stderr(message);
if (stackTrace case String(isNotEmpty: true)) {
log.stderr(stackTrace);
}
}
void _handleShowMessage(ShowMessageParams message) {
if (message.type == MessageType.Error) {
_handleServerError(message.message);
}
}
Stream<T> _stream<T, R>(Method method, T Function(R) converter) {
final controller = _streamControllers.putIfAbsent(
method,
() => StreamController<NotificationMessage>.broadcast(),
);
return controller.stream
.map((message) => message.params)
.cast<R>()
.map(converter);
}
bool dispose() {
return _process?.kill() ?? true;
}
Future<T> _expectSuccessfulResponse<T, R>(
Method method,
Object? params,
T Function(R) converter,
) async {
var response = await _sendRequest(method, params);
if (response.error case var error?) {
throw _RequestError(error);
}
return converter(response.result as R);
}
}
class _RequestError implements Exception {
final ResponseError error;
_RequestError(this.error);
@override
String toString() => 'LspException: ${error.code}: ${error.message}';
}