analysis.dart 10.4 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 6
// @dart = 2.8

7
import 'dart:async';
8
import 'dart:math' as math;
9

10
import 'package:meta/meta.dart';
11
import 'package:process/process.dart';
12

13
import '../base/common.dart';
14
import '../base/file_system.dart';
15
import '../base/io.dart';
16
import '../base/logger.dart';
17
import '../base/platform.dart';
18
import '../base/terminal.dart';
19
import '../base/utils.dart';
20
import '../convert.dart';
21

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

  final String sdkPath;
  final List<String> directories;
42 43 44 45
  final FileSystem _fileSystem;
  final ProcessManager _processManager;
  final Logger _logger;
  final Platform _platform;
46
  final Terminal _terminal;
47
  final String _protocolTrafficLog;
48 49 50

  Process _process;
  final StreamController<bool> _analyzingController =
51
      StreamController<bool>.broadcast();
52
  final StreamController<FileAnalysisErrors> _errorsController =
53
      StreamController<FileAnalysisErrors>.broadcast();
54
  bool _didServerErrorOccur = false;
55 56 57

  int _id = 0;

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

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

82 83 84
    final Stream<String> errorStream = _process.stderr
        .transform<String>(utf8.decoder)
        .transform<String>(const LineSplitter());
85
    errorStream.listen(_logger.printError);
86

87 88 89
    final Stream<String> inStream = _process.stdout
        .transform<String>(utf8.decoder)
        .transform<String>(const LineSplitter());
90 91 92
    inStream.listen(_handleServerResponse);

    _sendCommand('server.setSubscriptions', <String, dynamic>{
93
      'subscriptions': <String>['STATUS'],
94 95 96 97
    });

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

100
  bool get didServerErrorOccur => _didServerErrorOccur;
101

102
  Stream<bool> get onAnalyzing => _analyzingController.stream;
103

104
  Stream<FileAnalysisErrors> get onErrors => _errorsController.stream;
105

106
  Future<int> get onExit => _process.exitCode;
107

108 109 110 111
  void _sendCommand(String method, Map<String, dynamic> params) {
    final String message = json.encode(<String, dynamic>{
      'id': (++_id).toString(),
      'method': method,
112
      'params': params,
113 114
    });
    _process.stdin.writeln(message);
115
    _logger.printTrace('==> $message');
116
  }
117

118
  void _handleServerResponse(String line) {
119
    _logger.printTrace('<== $line');
120 121 122

    final dynamic response = json.decode(line);

123
    if (response is Map<String, dynamic>) {
124
      if (response['event'] != null) {
125
        final String event = response['event'] as String;
126 127
        final dynamic params = response['params'];

128
        if (params is Map<String, dynamic>) {
129
          if (event == 'server.status') {
130
            _handleStatus(castStringKeyedMap(response['params']));
131
          } else if (event == 'analysis.errors') {
132
            _handleAnalysisIssues(castStringKeyedMap(response['params']));
133
          } else if (event == 'server.error') {
134
            _handleServerError(castStringKeyedMap(response['params']));
135
          }
136 137 138
        }
      } else if (response['error'] != null) {
        // Fields are 'code', 'message', and 'stackTrace'.
139
        final Map<String, dynamic> error = castStringKeyedMap(response['error']);
140
        _logger.printError(
141 142
            'Error response from the server: ${error['code']} ${error['message']}');
        if (error['stackTrace'] != null) {
143
          _logger.printError(error['stackTrace'] as String);
144
        }
145
      }
146
    }
147
  }
148

149 150 151
  void _handleStatus(Map<String, dynamic> statusInfo) {
    // {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
    if (statusInfo['analysis'] != null && !_analyzingController.isClosed) {
152
      final bool isAnalyzing = statusInfo['analysis']['isAnalyzing'] as bool;
153
      _analyzingController.add(isAnalyzing);
154
    }
155 156
  }

157 158
  void _handleServerError(Map<String, dynamic> error) {
    // Fields are 'isFatal', 'message', and 'stackTrace'.
159
    _logger.printError('Error from the analysis server: ${error['message']}');
160
    if (error['stackTrace'] != null) {
161
      _logger.printError(error['stackTrace'] as String);
162
    }
163
    _didServerErrorOccur = true;
164 165
  }

166 167
  void _handleAnalysisIssues(Map<String, dynamic> issueInfo) {
    // {"event":"analysis.errors","params":{"file":"/Users/.../lib/main.dart","errors":[]}}
168 169
    final String file = issueInfo['file'] as String;
    final List<dynamic> errorsList = issueInfo['errors'] as List<dynamic>;
170 171
    final List<AnalysisError> errors = errorsList
        .map<Map<String, dynamic>>(castStringKeyedMap)
172
        .map<AnalysisError>((Map<String, dynamic> json) {
173
          return AnalysisError(WrittenError.fromJson(json),
174 175 176 177 178
            fileSystem: _fileSystem,
            platform: _platform,
            terminal: _terminal,
          );
        })
179
        .toList();
180
    if (!_errorsController.isClosed) {
181
      _errorsController.add(FileAnalysisErrors(file, errors));
182
    }
183 184
  }

185 186 187 188
  Future<bool> dispose() async {
    await _analyzingController.close();
    await _errorsController.close();
    return _process?.kill();
189 190 191
  }
}

192
enum AnalysisSeverity {
193 194 195 196 197 198
  error,
  warning,
  info,
  none,
}

199
/// [AnalysisError] with command line style.
200
class AnalysisError implements Comparable<AnalysisError> {
201 202
  AnalysisError(
    this.writtenError, {
203
    @required Platform platform,
204
    @required Terminal terminal,
205 206 207 208 209
    @required FileSystem fileSystem,
  }) : _platform = platform,
       _terminal = terminal,
       _fileSystem = fileSystem;

210
  final WrittenError writtenError;
211
  final Platform _platform;
212
  final Terminal _terminal;
213
  final FileSystem _fileSystem;
214

215
  String get _separator => _platform.isWindows ? '-' : '•';
216

217
  String get colorSeverity {
218
    switch (writtenError.severityLevel) {
219
      case AnalysisSeverity.error:
220
        return _terminal.color(writtenError.severity, TerminalColor.red);
221
      case AnalysisSeverity.warning:
222
        return _terminal.color(writtenError.severity, TerminalColor.yellow);
223 224
      case AnalysisSeverity.info:
      case AnalysisSeverity.none:
225
        return writtenError.severity;
226 227 228
    }
    return null;
  }
229

230 231
  String get type => writtenError.type;
  String get code => writtenError.code;
232

233 234 235
  @override
  int compareTo(AnalysisError other) {
    // Sort in order of file path, error location, severity, and message.
236 237
    if (writtenError.file != other.writtenError.file) {
      return writtenError.file.compareTo(other.writtenError.file);
238
    }
239

240 241
    if (writtenError.offset != other.writtenError.offset) {
      return writtenError.offset - other.writtenError.offset;
242
    }
243

244 245
    final int diff = other.writtenError.severityLevel.index -
        writtenError.severityLevel.index;
246
    if (diff != 0) {
247
      return diff;
248
    }
249

250
    return writtenError.message.compareTo(other.writtenError.message);
251 252
  }

253 254
  @override
  String toString() {
255 256
    // Can't use "padLeft" because of ANSI color sequences in the colorized
    // severity.
257
    final String padding = ' ' * math.max(0, 7 - writtenError.severity.length);
258
    return '$padding${colorSeverity.toLowerCase()} $_separator '
259 260
        '${writtenError.messageSentenceFragment} $_separator '
        '${_fileSystem.path.relative(writtenError.file)}:${writtenError.startLine}:${writtenError.startColumn} $_separator '
261
        '$code';
262
  }
263

264
  String toLegacyString() {
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
    return writtenError.toString();
  }
}

/// [AnalysisError] in plain text content.
class WrittenError {
  WrittenError._({
    @required this.severity,
    @required this.type,
    @required this.message,
    @required this.code,
    @required this.file,
    @required this.startLine,
    @required this.startColumn,
    @required this.offset,
  });

  ///  {
  ///      "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) {
    return WrittenError._(
      severity: json['severity'] as String,
      type: json['type'] as String,
      message: json['message'] as String,
      code: json['code'] as String,
      file: json['location']['file'] as String,
      startLine: json['location']['startLine'] as int,
      startColumn: json['location']['startColumn'] as int,
      offset: json['location']['offset'] as int,
    );
  }

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

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

318 319 320 321
  static final Map<String, AnalysisSeverity> _severityMap = <String, AnalysisSeverity>{
    'INFO': AnalysisSeverity.info,
    'WARNING': AnalysisSeverity.warning,
    'ERROR': AnalysisSeverity.error,
322 323
  };

324 325
  AnalysisSeverity get severityLevel =>
      _severityMap[severity] ?? AnalysisSeverity.none;
326 327 328 329 330 331 332 333 334 335

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

  @override
  String toString() {
336 337
    return '[${severity.toLowerCase()}] $messageSentenceFragment ($file:$startLine:$startColumn)';
  }
338 339
}

340 341
class FileAnalysisErrors {
  FileAnalysisErrors(this.file, this.errors);
342

343 344
  final String file;
  final List<AnalysisError> errors;
345
}