analysis.dart 11.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6
import 'dart:math' as math;
7

8
import 'package:process/process.dart';
9

10
import '../base/common.dart';
11
import '../base/file_system.dart';
12
import '../base/io.dart';
13
import '../base/logger.dart';
14
import '../base/platform.dart';
15
import '../base/terminal.dart';
16
import '../base/utils.dart';
17
import '../convert.dart';
18

19
/// An interface to the Dart analysis server.
20
class AnalysisServer {
21 22 23
  AnalysisServer(
    this.sdkPath,
    this.directories, {
24 25 26 27 28 29
    required FileSystem fileSystem,
    required ProcessManager processManager,
    required Logger logger,
    required Platform platform,
    required Terminal terminal,
    String? protocolTrafficLog,
30 31 32 33
  }) : _fileSystem = fileSystem,
       _processManager = processManager,
       _logger = logger,
       _platform = platform,
34 35
       _terminal = terminal,
       _protocolTrafficLog = protocolTrafficLog;
36 37 38

  final String sdkPath;
  final List<String> directories;
39 40 41 42
  final FileSystem _fileSystem;
  final ProcessManager _processManager;
  final Logger _logger;
  final Platform _platform;
43
  final Terminal _terminal;
44
  final String? _protocolTrafficLog;
45

46
  Process? _process;
47
  final StreamController<bool> _analyzingController =
48
      StreamController<bool>.broadcast();
49
  final StreamController<FileAnalysisErrors> _errorsController =
50
      StreamController<FileAnalysisErrors>.broadcast();
51
  bool _didServerErrorOccur = false;
52 53 54

  int _id = 0;

55
  Future<void> start() async {
56 57 58 59 60 61
    final String snapshot = _fileSystem.path.join(
      sdkPath,
      'bin',
      'snapshots',
      'analysis_server.dart.snapshot',
    );
62
    final List<String> command = <String>[
63
      _fileSystem.path.join(sdkPath, 'bin', 'dart'),
64
      '--disable-dart-dev',
65
      snapshot,
66 67
      '--disable-server-feature-completion',
      '--disable-server-feature-search',
68 69
      '--sdk',
      sdkPath,
70 71
      if (_protocolTrafficLog != null)
        '--protocol-traffic-log=$_protocolTrafficLog',
72 73
    ];

74 75
    _logger.printTrace('dart ${command.skip(1).join(' ')}');
    _process = await _processManager.start(command);
76
    // This callback hookup can't throw.
77
    unawaited(_process!.exitCode.whenComplete(() => _process = null));
78

79
    final Stream<String> errorStream = _process!.stderr
80 81
        .transform<String>(utf8.decoder)
        .transform<String>(const LineSplitter());
82
    errorStream.listen(_handleError);
83

84
    final Stream<String> inStream = _process!.stdout
85 86
        .transform<String>(utf8.decoder)
        .transform<String>(const LineSplitter());
87 88 89
    inStream.listen(_handleServerResponse);

    _sendCommand('server.setSubscriptions', <String, dynamic>{
90
      'subscriptions': <String>['STATUS'],
91 92 93 94
    });

    _sendCommand('analysis.setAnalysisRoots',
        <String, dynamic>{'included': directories, 'excluded': <String>[]});
95
  }
96

97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
  final List<String> _logs = <String>[];

  /// Aggregated STDOUT and STDERR logs from the server.
  ///
  /// This can be surfaced to the user if the server crashes. If [tail] is null,
  /// returns all logs, else only the last [tail] lines.
  String getLogs([int? tail]) {
    if (tail == null) {
      return _logs.join('\n');
    }
    // Since List doesn't implement a .tail() method, we reverse it then use
    // .take()
    final Iterable<String> reversedLogs = _logs.reversed;
    final List<String> firstTailLogs = reversedLogs.take(tail).toList();
    return firstTailLogs.reversed.join('\n');
  }

  void _handleError(String message) {
    _logs.add('[stderr] $message');
    _logger.printError(message);
  }

119
  bool get didServerErrorOccur => _didServerErrorOccur;
120

121
  Stream<bool> get onAnalyzing => _analyzingController.stream;
122

123
  Stream<FileAnalysisErrors> get onErrors => _errorsController.stream;
124

125
  Future<int?> get onExit async => _process?.exitCode;
126

127 128 129 130
  void _sendCommand(String method, Map<String, dynamic> params) {
    final String message = json.encode(<String, dynamic>{
      'id': (++_id).toString(),
      'method': method,
131
      'params': params,
132
    });
133
    _process?.stdin.writeln(message);
134
    _logger.printTrace('==> $message');
135
  }
136

137
  void _handleServerResponse(String line) {
138
    _logs.add('[stdout] $line');
139
    _logger.printTrace('<== $line');
140 141 142

    final dynamic response = json.decode(line);

143
    if (response is Map<String, dynamic>) {
144
      if (response['event'] != null) {
145
        final String event = response['event'] as String;
146
        final dynamic params = response['params'];
147
        Map<String, dynamic>? paramsMap;
148
        if (params is Map<String, dynamic>) {
149 150 151 152
          paramsMap = castStringKeyedMap(params);
        }

        if (paramsMap != null) {
153
          if (event == 'server.status') {
154
            _handleStatus(paramsMap);
155
          } else if (event == 'analysis.errors') {
156
            _handleAnalysisIssues(paramsMap);
157
          } else if (event == 'server.error') {
158
            _handleServerError(paramsMap);
159
          }
160 161 162
        }
      } else if (response['error'] != null) {
        // Fields are 'code', 'message', and 'stackTrace'.
163
        final Map<String, dynamic> error = castStringKeyedMap(response['error'])!;
164
        _logger.printError(
165 166
            'Error response from the server: ${error['code']} ${error['message']}');
        if (error['stackTrace'] != null) {
167
          _logger.printError(error['stackTrace'] as String);
168
        }
169
      }
170
    }
171
  }
172

173 174 175
  void _handleStatus(Map<String, dynamic> statusInfo) {
    // {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
    if (statusInfo['analysis'] != null && !_analyzingController.isClosed) {
176
      final bool isAnalyzing = (statusInfo['analysis'] as Map<String, dynamic>)['isAnalyzing'] as bool;
177
      _analyzingController.add(isAnalyzing);
178
    }
179 180
  }

181 182
  void _handleServerError(Map<String, dynamic> error) {
    // Fields are 'isFatal', 'message', and 'stackTrace'.
183
    _logger.printError('Error from the analysis server: ${error['message']}');
184
    if (error['stackTrace'] != null) {
185
      _logger.printError(error['stackTrace'] as String);
186
    }
187
    _didServerErrorOccur = true;
188 189
  }

190 191
  void _handleAnalysisIssues(Map<String, dynamic> issueInfo) {
    // {"event":"analysis.errors","params":{"file":"/Users/.../lib/main.dart","errors":[]}}
192 193
    final String file = issueInfo['file'] as String;
    final List<dynamic> errorsList = issueInfo['errors'] as List<dynamic>;
194
    final List<AnalysisError> errors = errorsList
195
        .map<Map<String, dynamic>>((dynamic e) => castStringKeyedMap(e) ?? <String, dynamic>{})
196
        .map<AnalysisError>((Map<String, dynamic> json) {
197
          return AnalysisError(WrittenError.fromJson(json),
198 199 200 201 202
            fileSystem: _fileSystem,
            platform: _platform,
            terminal: _terminal,
          );
        })
203
        .toList();
204
    if (!_errorsController.isClosed) {
205
      _errorsController.add(FileAnalysisErrors(file, errors));
206
    }
207 208
  }

209
  Future<bool?> dispose() async {
210 211 212
    await _analyzingController.close();
    await _errorsController.close();
    return _process?.kill();
213 214 215
  }
}

216
enum AnalysisSeverity {
217 218 219 220 221 222
  error,
  warning,
  info,
  none,
}

223
/// [AnalysisError] with command line style.
224
class AnalysisError implements Comparable<AnalysisError> {
225 226
  AnalysisError(
    this.writtenError, {
227 228 229
    required Platform platform,
    required Terminal terminal,
    required FileSystem fileSystem,
230 231 232 233
  }) : _platform = platform,
       _terminal = terminal,
       _fileSystem = fileSystem;

234
  final WrittenError writtenError;
235
  final Platform _platform;
236
  final Terminal _terminal;
237
  final FileSystem _fileSystem;
238

239
  String get _separator => _platform.isWindows ? '-' : '•';
240

241
  String get colorSeverity {
242
    switch (writtenError.severityLevel) {
243
      case AnalysisSeverity.error:
244
        return _terminal.color(writtenError.severity, TerminalColor.red);
245
      case AnalysisSeverity.warning:
246
        return _terminal.color(writtenError.severity, TerminalColor.yellow);
247 248
      case AnalysisSeverity.info:
      case AnalysisSeverity.none:
249
        return writtenError.severity;
250 251
    }
  }
252

253 254
  String get type => writtenError.type;
  String get code => writtenError.code;
255

256 257 258
  @override
  int compareTo(AnalysisError other) {
    // Sort in order of file path, error location, severity, and message.
259 260
    if (writtenError.file != other.writtenError.file) {
      return writtenError.file.compareTo(other.writtenError.file);
261
    }
262

263 264
    if (writtenError.offset != other.writtenError.offset) {
      return writtenError.offset - other.writtenError.offset;
265
    }
266

267 268
    final int diff = other.writtenError.severityLevel.index -
        writtenError.severityLevel.index;
269
    if (diff != 0) {
270
      return diff;
271
    }
272

273
    return writtenError.message.compareTo(other.writtenError.message);
274 275
  }

276 277
  @override
  String toString() {
278 279
    // Can't use "padLeft" because of ANSI color sequences in the colorized
    // severity.
280
    final String padding = ' ' * math.max(0, 7 - writtenError.severity.length);
281
    return '$padding${colorSeverity.toLowerCase()} $_separator '
282 283
        '${writtenError.messageSentenceFragment} $_separator '
        '${_fileSystem.path.relative(writtenError.file)}:${writtenError.startLine}:${writtenError.startColumn} $_separator '
284
        '$code';
285
  }
286

287
  String toLegacyString() {
288 289 290 291 292 293 294
    return writtenError.toString();
  }
}

/// [AnalysisError] in plain text content.
class WrittenError {
  WrittenError._({
295 296 297 298 299 300 301 302
    required this.severity,
    required this.type,
    required this.message,
    required this.code,
    required this.file,
    required this.startLine,
    required this.startColumn,
    required this.offset,
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
  });

  ///  {
  ///      "severity":"INFO",
  ///      "type":"TODO",
  ///      "location":{
  ///          "file":"/Users/.../lib/test.dart",
  ///          "offset":362,
  ///          "length":72,
  ///          "startLine":15,
  ///         "startColumn":4
  ///      },
  ///      "message":"...",
  ///      "hasFix":false
  ///  }
  static WrittenError fromJson(Map<String, dynamic> json) {
319
    final Map<String, dynamic> location = json['location'] as Map<String, dynamic>;
320 321 322 323 324
    return WrittenError._(
      severity: json['severity'] as String,
      type: json['type'] as String,
      message: json['message'] as String,
      code: json['code'] as String,
325 326 327 328
      file: location['file'] as String,
      startLine: location['startLine'] as int,
      startColumn: location['startColumn'] as int,
      offset: location['offset'] as int,
329 330 331 332 333 334 335 336 337 338 339 340 341
    );
  }

  final String severity;
  final String type;
  final String message;
  final String code;

  final String file;
  final int startLine;
  final int startColumn;
  final int offset;

342 343 344 345
  static final Map<String, AnalysisSeverity> _severityMap = <String, AnalysisSeverity>{
    'INFO': AnalysisSeverity.info,
    'WARNING': AnalysisSeverity.warning,
    'ERROR': AnalysisSeverity.error,
346 347
  };

348 349
  AnalysisSeverity get severityLevel =>
      _severityMap[severity] ?? AnalysisSeverity.none;
350 351 352 353 354 355 356 357 358 359

  String get messageSentenceFragment {
    if (message.endsWith('.')) {
      return message.substring(0, message.length - 1);
    }
    return message;
  }

  @override
  String toString() {
360 361
    return '[${severity.toLowerCase()}] $messageSentenceFragment ($file:$startLine:$startColumn)';
  }
362 363
}

364 365
class FileAnalysisErrors {
  FileAnalysisErrors(this.file, this.errors);
366

367 368
  final String file;
  final List<AnalysisError> errors;
369
}