| // Copyright 2016 The Chromium Authors. 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:math' as math; |
| |
| import '../base/file_system.dart' hide IOSink; |
| import '../base/file_system.dart'; |
| import '../base/io.dart'; |
| import '../base/platform.dart'; |
| import '../base/process_manager.dart'; |
| import '../base/terminal.dart'; |
| import '../base/utils.dart'; |
| import '../globals.dart'; |
| |
| class AnalysisServer { |
| AnalysisServer(this.sdkPath, this.directories); |
| |
| final String sdkPath; |
| final List<String> directories; |
| |
| Process _process; |
| final StreamController<bool> _analyzingController = |
| StreamController<bool>.broadcast(); |
| final StreamController<FileAnalysisErrors> _errorsController = |
| StreamController<FileAnalysisErrors>.broadcast(); |
| |
| int _id = 0; |
| |
| Future<void> start() async { |
| final String snapshot = |
| fs.path.join(sdkPath, 'bin/snapshots/analysis_server.dart.snapshot'); |
| final List<String> command = <String>[ |
| fs.path.join(sdkPath, 'bin', 'dart'), |
| snapshot, |
| '--sdk', |
| sdkPath, |
| ]; |
| |
| printTrace('dart ${command.skip(1).join(' ')}'); |
| _process = await processManager.start(command); |
| // This callback hookup can't throw. |
| _process.exitCode.whenComplete(() => _process = null); // ignore: unawaited_futures |
| |
| final Stream<String> errorStream = |
| _process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()); |
| errorStream.listen(printError); |
| |
| final Stream<String> inStream = |
| _process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()); |
| inStream.listen(_handleServerResponse); |
| |
| _sendCommand('server.setSubscriptions', <String, dynamic>{ |
| 'subscriptions': <String>['STATUS'] |
| }); |
| |
| _sendCommand('analysis.setAnalysisRoots', |
| <String, dynamic>{'included': directories, 'excluded': <String>[]}); |
| } |
| |
| Stream<bool> get onAnalyzing => _analyzingController.stream; |
| Stream<FileAnalysisErrors> get onErrors => _errorsController.stream; |
| |
| Future<int> get onExit => _process.exitCode; |
| |
| void _sendCommand(String method, Map<String, dynamic> params) { |
| final String message = json.encode(<String, dynamic>{ |
| 'id': (++_id).toString(), |
| 'method': method, |
| 'params': params |
| }); |
| _process.stdin.writeln(message); |
| printTrace('==> $message'); |
| } |
| |
| void _handleServerResponse(String line) { |
| printTrace('<== $line'); |
| |
| final dynamic response = json.decode(line); |
| |
| if (response is Map<dynamic, dynamic>) { |
| if (response['event'] != null) { |
| final String event = response['event']; |
| final dynamic params = response['params']; |
| |
| if (params is Map<dynamic, dynamic>) { |
| if (event == 'server.status') |
| _handleStatus(response['params']); |
| else if (event == 'analysis.errors') |
| _handleAnalysisIssues(response['params']); |
| else if (event == 'server.error') |
| _handleServerError(response['params']); |
| } |
| } else if (response['error'] != null) { |
| // Fields are 'code', 'message', and 'stackTrace'. |
| final Map<String, dynamic> error = response['error']; |
| printError( |
| 'Error response from the server: ${error['code']} ${error['message']}'); |
| if (error['stackTrace'] != null) { |
| printError(error['stackTrace']); |
| } |
| } |
| } |
| } |
| |
| void _handleStatus(Map<String, dynamic> statusInfo) { |
| // {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}} |
| if (statusInfo['analysis'] != null && !_analyzingController.isClosed) { |
| final bool isAnalyzing = statusInfo['analysis']['isAnalyzing']; |
| _analyzingController.add(isAnalyzing); |
| } |
| } |
| |
| void _handleServerError(Map<String, dynamic> error) { |
| // Fields are 'isFatal', 'message', and 'stackTrace'. |
| printError('Error from the analysis server: ${error['message']}'); |
| if (error['stackTrace'] != null) { |
| printError(error['stackTrace']); |
| } |
| } |
| |
| void _handleAnalysisIssues(Map<String, dynamic> issueInfo) { |
| // {"event":"analysis.errors","params":{"file":"/Users/.../lib/main.dart","errors":[]}} |
| final String file = issueInfo['file']; |
| final List<dynamic> errorsList = issueInfo['errors']; |
| final List<AnalysisError> errors = errorsList |
| .map<Map<String, dynamic>>(castStringKeyedMap) |
| .map<AnalysisError>((Map<String, dynamic> json) => AnalysisError(json)) |
| .toList(); |
| if (!_errorsController.isClosed) |
| _errorsController.add(FileAnalysisErrors(file, errors)); |
| } |
| |
| Future<bool> dispose() async { |
| await _analyzingController.close(); |
| await _errorsController.close(); |
| return _process?.kill(); |
| } |
| } |
| |
| 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, |
| }; |
| |
| static final String _separator = platform.isWindows ? '-' : '•'; |
| |
| // "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']; |
| String get colorSeverity { |
| switch(_severityLevel) { |
| case _AnalysisSeverity.error: |
| return terminal.color(severity, TerminalColor.red); |
| case _AnalysisSeverity.warning: |
| return terminal.color(severity, TerminalColor.yellow); |
| case _AnalysisSeverity.info: |
| case _AnalysisSeverity.none: |
| return severity; |
| } |
| return null; |
| } |
| _AnalysisSeverity get _severityLevel => _severityMap[severity] ?? _AnalysisSeverity.none; |
| String get type => json['type']; |
| String get message => json['message']; |
| String get code => json['code']; |
| |
| String get file => json['location']['file']; |
| int get startLine => json['location']['startLine']; |
| int get startColumn => json['location']['startColumn']; |
| int get offset => json['location']['offset']; |
| |
| String get messageSentenceFragment { |
| if (message.endsWith('.')) { |
| return message.substring(0, message.length - 1); |
| } else { |
| return message; |
| } |
| } |
| |
| @override |
| int compareTo(AnalysisError other) { |
| // Sort in order of file path, error location, severity, and message. |
| if (file != other.file) |
| return file.compareTo(other.file); |
| |
| if (offset != other.offset) |
| return offset - other.offset; |
| |
| final int diff = other._severityLevel.index - _severityLevel.index; |
| if (diff != 0) |
| return diff; |
| |
| return message.compareTo(other.message); |
| } |
| |
| @override |
| String toString() { |
| // Can't use "padLeft" because of ANSI color sequences in the colorized |
| // severity. |
| final String padding = ' ' * math.max(0, 7 - severity.length); |
| return '$padding${colorSeverity.toLowerCase()} $_separator ' |
| '$messageSentenceFragment $_separator ' |
| '${fs.path.relative(file)}:$startLine:$startColumn $_separator ' |
| '$code'; |
| } |
| |
| String toLegacyString() { |
| return '[${severity.toLowerCase()}] $messageSentenceFragment ($file:$startLine:$startColumn)'; |
| } |
| } |
| |
| class FileAnalysisErrors { |
| FileAnalysisErrors(this.file, this.errors); |
| |
| final String file; |
| final List<AnalysisError> errors; |
| } |