analysis.dart 10.1 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
    @required Terminal terminal,
30 31 32 33
  }) : _fileSystem = fileSystem,
       _processManager = processManager,
       _logger = logger,
       _platform = platform,
34
       _terminal = terminal;
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 44 45

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

  int _id = 0;

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

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

75 76 77
    final Stream<String> errorStream = _process.stderr
        .transform<String>(utf8.decoder)
        .transform<String>(const LineSplitter());
78
    errorStream.listen(_logger.printError);
79

80 81 82
    final Stream<String> inStream = _process.stdout
        .transform<String>(utf8.decoder)
        .transform<String>(const LineSplitter());
83 84 85
    inStream.listen(_handleServerResponse);

    _sendCommand('server.setSubscriptions', <String, dynamic>{
86
      'subscriptions': <String>['STATUS'],
87 88 89 90
    });

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

93
  bool get didServerErrorOccur => _didServerErrorOccur;
94

95
  Stream<bool> get onAnalyzing => _analyzingController.stream;
96

97
  Stream<FileAnalysisErrors> get onErrors => _errorsController.stream;
98

99
  Future<int> get onExit => _process.exitCode;
100

101 102 103 104
  void _sendCommand(String method, Map<String, dynamic> params) {
    final String message = json.encode(<String, dynamic>{
      'id': (++_id).toString(),
      'method': method,
105
      'params': params,
106 107
    });
    _process.stdin.writeln(message);
108
    _logger.printTrace('==> $message');
109
  }
110

111
  void _handleServerResponse(String line) {
112
    _logger.printTrace('<== $line');
113 114 115

    final dynamic response = json.decode(line);

116
    if (response is Map<String, dynamic>) {
117
      if (response['event'] != null) {
118
        final String event = response['event'] as String;
119 120
        final dynamic params = response['params'];

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

142 143 144
  void _handleStatus(Map<String, dynamic> statusInfo) {
    // {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
    if (statusInfo['analysis'] != null && !_analyzingController.isClosed) {
145
      final bool isAnalyzing = statusInfo['analysis']['isAnalyzing'] as bool;
146
      _analyzingController.add(isAnalyzing);
147
    }
148 149
  }

150 151
  void _handleServerError(Map<String, dynamic> error) {
    // Fields are 'isFatal', 'message', and 'stackTrace'.
152
    _logger.printError('Error from the analysis server: ${error['message']}');
153
    if (error['stackTrace'] != null) {
154
      _logger.printError(error['stackTrace'] as String);
155
    }
156
    _didServerErrorOccur = true;
157 158
  }

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

178 179 180 181
  Future<bool> dispose() async {
    await _analyzingController.close();
    await _errorsController.close();
    return _process?.kill();
182 183 184
  }
}

185
enum AnalysisSeverity {
186 187 188 189 190 191
  error,
  warning,
  info,
  none,
}

192
/// [AnalysisError] with command line style.
193
class AnalysisError implements Comparable<AnalysisError> {
194 195
  AnalysisError(
    this.writtenError, {
196
    @required Platform platform,
197
    @required Terminal terminal,
198 199 200 201 202
    @required FileSystem fileSystem,
  }) : _platform = platform,
       _terminal = terminal,
       _fileSystem = fileSystem;

203
  final WrittenError writtenError;
204
  final Platform _platform;
205
  final Terminal _terminal;
206
  final FileSystem _fileSystem;
207

208
  String get _separator => _platform.isWindows ? '-' : '•';
209

210
  String get colorSeverity {
211
    switch (writtenError.severityLevel) {
212
      case AnalysisSeverity.error:
213
        return _terminal.color(writtenError.severity, TerminalColor.red);
214
      case AnalysisSeverity.warning:
215
        return _terminal.color(writtenError.severity, TerminalColor.yellow);
216 217
      case AnalysisSeverity.info:
      case AnalysisSeverity.none:
218
        return writtenError.severity;
219 220 221
    }
    return null;
  }
222

223 224
  String get type => writtenError.type;
  String get code => writtenError.code;
225

226 227 228
  @override
  int compareTo(AnalysisError other) {
    // Sort in order of file path, error location, severity, and message.
229 230
    if (writtenError.file != other.writtenError.file) {
      return writtenError.file.compareTo(other.writtenError.file);
231
    }
232

233 234
    if (writtenError.offset != other.writtenError.offset) {
      return writtenError.offset - other.writtenError.offset;
235
    }
236

237 238
    final int diff = other.writtenError.severityLevel.index -
        writtenError.severityLevel.index;
239
    if (diff != 0) {
240
      return diff;
241
    }
242

243
    return writtenError.message.compareTo(other.writtenError.message);
244 245
  }

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

257
  String toLegacyString() {
258 259 260 261 262 263 264 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
    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;

311 312 313 314
  static final Map<String, AnalysisSeverity> _severityMap = <String, AnalysisSeverity>{
    'INFO': AnalysisSeverity.info,
    'WARNING': AnalysisSeverity.warning,
    'ERROR': AnalysisSeverity.error,
315 316
  };

317 318
  AnalysisSeverity get severityLevel =>
      _severityMap[severity] ?? AnalysisSeverity.none;
319 320 321 322 323 324 325 326 327 328

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

  @override
  String toString() {
329 330
    return '[${severity.toLowerCase()}] $messageSentenceFragment ($file:$startLine:$startColumn)';
  }
331 332
}

333 334
class FileAnalysisErrors {
  FileAnalysisErrors(this.file, this.errors);
335

336 337
  final String file;
  final List<AnalysisError> errors;
338
}