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
import 'dart:async';
6
import 'dart:math' as math;
7

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

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

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

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

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

  int _id = 0;

56
  Future<void> start() async {
57 58 59 60 61 62
    final String snapshot = _fileSystem.path.join(
      sdkPath,
      'bin',
      'snapshots',
      'analysis_server.dart.snapshot',
    );
63
    final List<String> command = <String>[
64
      _fileSystem.path.join(sdkPath, 'bin', 'dart'),
65
      '--disable-dart-dev',
66
      snapshot,
67 68 69 70 71
      for (String experiment in _experiments)
        ...<String>[
          '--enable-experiment',
          experiment,
        ],
72 73
      '--disable-server-feature-completion',
      '--disable-server-feature-search',
74 75 76 77
      '--sdk',
      sdkPath,
    ];

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

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

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

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

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

101
  bool get didServerErrorOccur => _didServerErrorOccur;
102

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

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

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

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

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

    final dynamic response = json.decode(line);

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

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

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

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

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

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

193 194 195 196 197 198 199
enum _AnalysisSeverity {
  error,
  warning,
  info,
  none,
}

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

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

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

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

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

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

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

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

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

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

265
  String toLegacyString() {
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 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
    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;

  static final Map<String, _AnalysisSeverity> _severityMap = <String, _AnalysisSeverity>{
    'INFO': _AnalysisSeverity.info,
    'WARNING': _AnalysisSeverity.warning,
    'ERROR': _AnalysisSeverity.error,
  };

  _AnalysisSeverity get severityLevel =>
      _severityMap[severity] ?? _AnalysisSeverity.none;

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

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

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

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