blob: 9494240797c5d27c7e27980987b51383320361b5 [file] [log] [blame] [edit]
// Copyright (c) 2020, 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/server/driver.dart' show Driver;
import 'package:analysis_server_client/protocol.dart'
show
AddContentOverlay,
AnalysisUpdateContentParams,
EditBulkFixesResult,
ResponseDecoder;
import 'package:args/args.dart';
import 'package:path/path.dart' as path;
import 'core.dart';
import 'experiments.dart';
import 'sdk.dart';
import 'utils.dart';
/// When set, this function is executed just before the Analysis Server starts.
void Function(String cmdName, List<FileSystemEntity> analysisRoots,
ArgResults? argResults)? preAnalysisServerStart;
/// A class to provide an API wrapper around an analysis server process.
class AnalysisServer {
AnalysisServer(
this.packagesFile,
this.sdkPath,
this.analysisRoots, {
this.cacheDirectoryPath,
required this.commandName,
required this.argResults,
this.enabledExperiments = const [],
this.disableStatusNotificationDebouncing = false,
this.suppressAnalytics = false,
bool useAotSnapshot = false,
}) : _useAotSnapshot = useAotSnapshot;
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 disableStatusNotificationDebouncing;
final bool suppressAnalytics;
final bool _useAotSnapshot;
Process? _process;
/// When not null, this is a [Completer] which completes when analysis has
/// finished, otherwise `null`.
Completer<bool>? _analysisFinished;
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;
Stream<bool> get onAnalyzing {
// {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
return _streamController('server.status')
.stream
.where((event) => event['analysis'] != null)
.map((event) => (event['analysis']['isAnalyzing']!) as bool);
}
/// This future completes when we next receive an analysis finished event
/// (unless there's no current analysis and we've already received a complete
/// event, in which case this future completes immediately).
Future<bool>? get analysisFinished => _analysisFinished?.future;
Stream<FileAnalysisErrors> get onErrors {
// {"event":"analysis.errors",
// "params":{"file":"/Users/.../lib/main.dart","errors":[]}}
return _streamController('analysis.errors').stream.map((event) {
final file = event['file'] as String;
final errorsList = event['errors'] as List<dynamic>;
final errors = [
for (final error in errorsList)
AnalysisError((error as Map).cast<String, dynamic>())
];
return FileAnalysisErrors(file, errors);
});
}
Future<int> get onExit => _process!.exitCode;
final Map<String, StreamController<Map<String, dynamic>>> _streamControllers =
{};
/// Completes when an analysis server crash has been detected.
Future<void> get onCrash => _onCrash.future;
final _onCrash = Completer<void>();
final Map<String, Completer<Map<String, dynamic>>> _requestCompleters = {};
/// Starts the process and returns the pid for it.
Future<int> start({bool setAnalysisRoots = true}) async {
preAnalysisServerStart?.call(commandName, analysisRoots, argResults);
final process = await _startProcess();
_process = process;
_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');
final analysisFinished = _analysisFinished;
if (analysisFinished != null && !analysisFinished.isCompleted) {
// Complete this completer in order to unstick the process.
analysisFinished.completeError(error);
}
// 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<String>(utf8.decoder)
.transform<String>(const LineSplitter());
inStream.listen(_handleServerResponse);
_streamController('server.error').stream.listen(_handleServerError);
_streamController('server.pluginError').stream.listen(_handlePluginError);
_sendCommand('server.setSubscriptions', params: <String, dynamic>{
'subscriptions': <String>['STATUS'],
});
// Reference and trim off any trailing slash, the Dart Analysis Server
// protocol throws an error (INVALID_FILE_PATH_FORMAT) if there is a
// trailing slash.
//
// The call to `absolute.resolveSymbolicLinksSync()` canonicalizes the path
// to be passed to the analysis server.
final analysisRootPaths = [
for (final root in analysisRoots)
trimEnd(
root.absolute.resolveSymbolicLinksSync(), path.context.separator),
];
onAnalyzing.listen((isAnalyzing) {
final analysisFinished = _analysisFinished;
if (isAnalyzing && (analysisFinished?.isCompleted ?? true)) {
// Start a new completer, to be completed when we receive the
// corresponding analysis complete event.
_analysisFinished = Completer();
} else if (!isAnalyzing &&
analysisFinished != null &&
!analysisFinished.isCompleted) {
analysisFinished.complete(true);
}
});
if (setAnalysisRoots) {
await _sendCommand('analysis.setAnalysisRoots', params: {
'included': analysisRootPaths,
'excluded': [],
});
}
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.SUPPRESS_ANALYTICS_FLAG}',
'--${Driver.CLIENT_ID}=dart-$commandName',
'--disable-server-feature-completion',
'--disable-server-feature-search',
if (disableStatusNotificationDebouncing)
'--disable-status-notification-debouncing',
'--disable-silent-analysis-exceptions',
'--sdk',
sdkPath.path,
if (cacheDirectoryPath != null) '--cache=$cacheDirectoryPath',
if (packagesFile != null) '--packages=${packagesFile!.path}',
if (enabledExperiments.isNotEmpty)
'--$experimentFlagName=${enabledExperiments.join(',')}',
];
log.trace('$executable ${arguments.join(' ')}');
return Process.start(executable, arguments);
}
Future<String> getVersion() {
return _sendCommand('server.getVersion')
.then((response) => response['version']);
}
Future<EditBulkFixesResult> requestBulkFixes(
String filePath, bool inTestMode, List<String> codes,
{bool updatePubspec = false}) {
return _sendCommand('edit.bulkFixes', params: <String, dynamic>{
'included': [path.canonicalize(filePath)],
'inTestMode': inTestMode,
'updatePubspec': updatePubspec,
if (codes.isNotEmpty) 'codes': codes,
}).then((result) {
return EditBulkFixesResult.fromJson(
ResponseDecoder(null), 'result', result);
});
}
Future<void> shutdown({Duration? timeout}) async {
// Request shutdown.
final Future<void> future =
_sendCommand('server.shutdown').then((Map<String, dynamic> value) {
_shutdownResponseReceived = true;
return;
});
await (timeout != null
? future.timeout(timeout, onTimeout: () {
log.stderr(
'The analysis server timed out while shutting down.');
})
: future)
.whenComplete(dispose);
}
/// Send an `analysis.updateContent` request with the given [files].
Future<void> updateContent(Map<String, AddContentOverlay> files) async {
await _sendCommand('analysis.updateContent',
params: AnalysisUpdateContentParams(files).toJson());
}
Future<Map<String, dynamic>> _sendCommand(String method,
{Map<String, dynamic>? params}) {
final String id = (++_id).toString();
final String message = json.encode(<String, dynamic>{
'id': id,
'method': method,
'params': params,
});
final Completer<Map<String, dynamic>> completer = Completer();
_requestCompleters[id] = completer;
_process!.stdin.writeln(message);
log.trace('==> $message');
return completer.future;
}
void _handlePluginError(Map<String, dynamic>? error) {
_serverErrorReceived = true;
final err = error!;
// No need for a preamble (like in _handleServerError); the message should
// have all of the context necessary.
log.stderr(err['message']);
final stackTrace = err['stackTrace'];
if (stackTrace is String && stackTrace.isNotEmpty) {
log.stderr(stackTrace);
}
}
void _handleServerResponse(String line) {
log.trace('<== $line');
final response = json.decode(line) as Object?;
if (response is Map<String, dynamic>) {
if (response
case {'event': final String event, 'params': final Object? params}) {
if (params is Map<String, dynamic>) {
_streamController(event).add(params.cast<String, dynamic>());
}
} else if (response case {'id': final String id}) {
if (response case {'error': final Map<String, Object?> error}) {
_requestCompleters.remove(id)?.completeError(
RequestError.parse(error.cast<String, dynamic>()));
} else {
_requestCompleters.remove(id)?.complete(response['result'] ?? {});
}
}
}
}
void _handleServerError(Map<String, dynamic>? error) {
_serverErrorReceived = true;
final err = error!;
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');
// Fields are 'isFatal', 'message', and 'stackTrace'.
log.stderr(err['message']);
final stackTrace = err['stackTrace'];
if (stackTrace is String && stackTrace.isNotEmpty) {
log.stderr(stackTrace);
}
}
StreamController<Map<String, dynamic>> _streamController(String streamId) {
return _streamControllers.putIfAbsent(
streamId, () => StreamController<Map<String, dynamic>>.broadcast());
}
Future<bool> dispose() async {
return _process?.kill() ?? true;
}
}
enum _AnalysisSeverity {
error,
warning,
info,
none,
}
class AnalysisError implements Comparable<AnalysisError> {
AnalysisError(this.json);
static final Map<String, _AnalysisSeverity> _severityMap =
<String, _AnalysisSeverity>{
'INFO': _AnalysisSeverity.info,
'WARNING': _AnalysisSeverity.warning,
'ERROR': _AnalysisSeverity.error,
};
// "severity":"INFO","type":"TODO","location":{
// "file":"/Users/.../lib/test.dart","offset":362,"length":72,"startLine":15,"startColumn":4
// },"message":"...","hasFix":false}
Map<String, dynamic> json;
String? get severity => json['severity'] as String?;
_AnalysisSeverity get _severityLevel =>
_severityMap[severity!] ?? _AnalysisSeverity.none;
bool get isInfo => _severityLevel == _AnalysisSeverity.info;
bool get isWarning => _severityLevel == _AnalysisSeverity.warning;
bool get isError => _severityLevel == _AnalysisSeverity.error;
String get type => json['type'] as String;
String get message => json['message'] as String;
String get code => json['code'] as String;
String? get correction => json['correction'] as String?;
int? get endColumn => json['location']['endColumn'] as int?;
int? get endLine => json['location']['endLine'] as int?;
String get file => json['location']['file'] as String;
int? get startLine => json['location']['startLine'] as int?;
int? get startColumn => json['location']['startColumn'] as int?;
int get offset => json['location']['offset'] as int;
int get length => json['location']['length'] as int;
String? get url => json['url'] as String?;
List<DiagnosticMessage> get contextMessages {
var messages = json['contextMessages'] as List<dynamic>?;
if (messages == null) {
// The field is optional, so we return an empty list as a default value.
return [];
}
return messages.map((message) => DiagnosticMessage(message)).toList();
}
@override
int compareTo(AnalysisError other) {
// Sort in order of severity, file path, error location, and message.
final int diff = _severityLevel.index - other._severityLevel.index;
if (diff != 0) {
return diff;
}
if (file != other.file) {
return file.compareTo(other.file);
}
if (offset != other.offset) {
return offset - other.offset;
}
return message.compareTo(other.message);
}
@override
String toString() => '${severity!.toLowerCase()} • '
'$message • $file:$startLine:$startColumn • '
'($code)';
}
class DiagnosticMessage {
final Map<String, dynamic> json;
DiagnosticMessage(this.json);
int? get column => json['location']['startColumn'] as int?;
int? get endColumn => json['location']['endColumn'] as int?;
int? get endLine => json['location']['endLine'] as int?;
String get filePath => json['location']['file'] as String;
int get length => json['location']['length'] as int;
int get line => json['location']['startLine'] as int;
String get message => json['message'] as String;
int get offset => json['location']['offset'] as int;
}
class FileAnalysisErrors {
final String file;
final List<AnalysisError> errors;
FileAnalysisErrors(this.file, this.errors);
}
class RequestError {
static RequestError parse(dynamic error) {
return RequestError(
error['code'],
error['message'],
stackTrace: error['stackTrace'],
);
}
final String code;
final String message;
final String stackTrace;
RequestError(this.code, this.message, {required this.stackTrace});
@override
String toString() => '[RequestError code: $code, message: $message]';
}