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/file_system.dart';
11
import '../base/io.dart';
12
import '../base/logger.dart';
13
import '../base/platform.dart';
14
import '../base/terminal.dart';
15
import '../base/utils.dart';
16
import '../convert.dart';
17

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

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

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

  int _id = 0;

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

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

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

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

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

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

96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
  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);
  }

118
  bool get didServerErrorOccur => _didServerErrorOccur;
119

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

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

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

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

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

    final dynamic response = json.decode(line);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  ///  {
  ///      "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) {
318
    final Map<String, dynamic> location = json['location'] as Map<String, dynamic>;
319 320 321 322 323
    return WrittenError._(
      severity: json['severity'] as String,
      type: json['type'] as String,
      message: json['message'] as String,
      code: json['code'] as String,
324 325 326 327
      file: location['file'] as String,
      startLine: location['startLine'] as int,
      startColumn: location['startColumn'] as int,
      offset: location['offset'] as int,
328 329 330 331 332 333 334 335 336 337 338 339 340
    );
  }

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

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

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

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

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

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

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

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