analysis.dart 10.5 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(_logger.printError);
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
  bool get didServerErrorOccur => _didServerErrorOccur;
98

99
  Stream<bool> get onAnalyzing => _analyzingController.stream;
100

101
  Stream<FileAnalysisErrors> get onErrors => _errorsController.stream;
102

103
  Future<int?> get onExit async => _process?.exitCode;
104

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

115
  void _handleServerResponse(String line) {
116
    _logger.printTrace('<== $line');
117 118 119

    final dynamic response = json.decode(line);

120
    if (response is Map<String, dynamic>) {
121
      if (response['event'] != null) {
122
        final String event = response['event'] as String;
123
        final dynamic params = response['params'];
124
        Map<String, dynamic>? paramsMap;
125
        if (params is Map<String, dynamic>) {
126 127 128 129
          paramsMap = castStringKeyedMap(params);
        }

        if (paramsMap != null) {
130
          if (event == 'server.status') {
131
            _handleStatus(paramsMap);
132
          } else if (event == 'analysis.errors') {
133
            _handleAnalysisIssues(paramsMap);
134
          } else if (event == 'server.error') {
135
            _handleServerError(paramsMap);
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'] as Map<String, dynamic>)['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
    final List<AnalysisError> errors = errorsList
172
        .map<Map<String, dynamic>>((dynamic e) => castStringKeyedMap(e) ?? <String, dynamic>{})
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
  Future<bool?> dispose() async {
187 188 189
    await _analyzingController.close();
    await _errorsController.close();
    return _process?.kill();
190 191 192
  }
}

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

200
/// [AnalysisError] with command line style.
201
class AnalysisError implements Comparable<AnalysisError> {
202 203
  AnalysisError(
    this.writtenError, {
204 205 206
    required Platform platform,
    required Terminal terminal,
    required FileSystem fileSystem,
207 208 209 210
  }) : _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

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
    return writtenError.toString();
  }
}

/// [AnalysisError] in plain text content.
class WrittenError {
  WrittenError._({
272 273 274 275 276 277 278 279
    required this.severity,
    required this.type,
    required this.message,
    required this.code,
    required this.file,
    required this.startLine,
    required this.startColumn,
    required this.offset,
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
  });

  ///  {
  ///      "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) {
296
    final Map<String, dynamic> location = json['location'] as Map<String, dynamic>;
297 298 299 300 301
    return WrittenError._(
      severity: json['severity'] as String,
      type: json['type'] as String,
      message: json['message'] as String,
      code: json['code'] as String,
302 303 304 305
      file: location['file'] as String,
      startLine: location['startLine'] as int,
      startColumn: location['startColumn'] as int,
      offset: location['offset'] as int,
306 307 308 309 310 311 312 313 314 315 316 317 318
    );
  }

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

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

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

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

  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
}