analyze_continuously.dart 11 KB
Newer Older
1 2 3 4 5 6 7
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';

8
import 'package:args/args.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/process_manager.dart';
15
import '../base/terminal.dart';
16 17 18 19
import '../base/utils.dart';
import '../cache.dart';
import '../dart/sdk.dart';
import '../globals.dart';
20
import 'analyze_base.dart';
21

22
class AnalyzeContinuously extends AnalyzeBase {
23
  AnalyzeContinuously(ArgResults argResults, this.repoPackages) : super(argResults);
24

25
  final List<Directory> repoPackages;
26

27 28 29 30 31 32 33 34 35
  String analysisTarget;
  bool firstAnalysis = true;
  Set<String> analyzedPaths = new Set<String>();
  Map<String, List<AnalysisError>> analysisErrors = <String, List<AnalysisError>>{};
  Stopwatch analysisTimer;
  int lastErrorCount = 0;
  Status analysisStatus;

  @override
36
  Future<Null> analyze() async {
37 38
    List<String> directories;

39 40
    if (argResults['dartdocs'])
      throwToolExit('The --dartdocs option is currently not supported when using --watch.');
41

42
    if (argResults['flutter-repo']) {
43 44 45
      final PackageDependencyTracker dependencies = new PackageDependencyTracker();
      dependencies.checkForConflictingDependencies(repoPackages, dependencies);
      directories = repoPackages.map((Directory dir) => dir.path).toList();
46 47 48
      analysisTarget = 'Flutter repository';
      printTrace('Analyzing Flutter repository:');
      for (String projectPath in directories)
49
        printTrace('  ${fs.path.relative(projectPath)}');
50
    } else {
51 52
      directories = <String>[fs.currentDirectory.path];
      analysisTarget = fs.currentDirectory.path;
53 54
    }

55
    final AnalysisServer server = new AnalysisServer(dartSdkPath, directories);
56 57 58 59 60 61 62 63
    server.onAnalyzing.listen((bool isAnalyzing) => _handleAnalysisStatus(server, isAnalyzing));
    server.onErrors.listen(_handleAnalysisErrors);

    Cache.releaseLockEarly();

    await server.start();
    final int exitCode = await server.onExit;

64
    final String message = 'Analysis server exited with code $exitCode.';
65 66 67
    if (exitCode != 0)
      throwToolExit(message, exitCode: exitCode);
    printStatus(message);
68 69 70 71 72 73 74 75 76 77 78
  }

  void _handleAnalysisStatus(AnalysisServer server, bool isAnalyzing) {
    if (isAnalyzing) {
      analysisStatus?.cancel();
      if (!firstAnalysis)
        printStatus('\n');
      analysisStatus = logger.startProgress('Analyzing $analysisTarget...');
      analyzedPaths.clear();
      analysisTimer = new Stopwatch()..start();
    } else {
Devon Carew's avatar
Devon Carew committed
79
      analysisStatus?.stop();
80 81 82 83 84
      analysisTimer.stop();

      logger.printStatus(terminal.clearScreen(), newline: false);

      // Remove errors for deleted files, sort, and print errors.
85
      final List<AnalysisError> errors = <AnalysisError>[];
86
      for (String path in analysisErrors.keys.toList()) {
87
        if (fs.isFileSync(path)) {
88
          errors.addAll(analysisErrors[path]);
89 90 91 92 93
        } else {
          analysisErrors.remove(path);
        }
      }

94
      errors.sort();
95

96
      for (AnalysisError error in errors) {
97 98 99 100 101
        printStatus(error.toString());
        if (error.code != null)
          printTrace('error code: ${error.code}');
      }

102
      dumpErrors(errors.map<String>((AnalysisError error) => error.toLegacyString()));
103 104 105 106

      // Print an analysis summary.
      String errorsMessage;

107
      final int issueCount = errors.length;
108
      final int issueDiff = issueCount - lastErrorCount;
109 110 111 112 113 114 115 116 117 118 119 120 121
      lastErrorCount = issueCount;

      if (firstAnalysis)
        errorsMessage = '$issueCount ${pluralize('issue', issueCount)} found';
      else if (issueDiff > 0)
        errorsMessage = '$issueCount ${pluralize('issue', issueCount)} found ($issueDiff new)';
      else if (issueDiff < 0)
        errorsMessage = '$issueCount ${pluralize('issue', issueCount)} found (${-issueDiff} fixed)';
      else if (issueCount != 0)
        errorsMessage = '$issueCount ${pluralize('issue', issueCount)} found';
      else
        errorsMessage = 'no issues found';

122 123
      final String files = '${analyzedPaths.length} ${pluralize('file', analyzedPaths.length)}';
      final String seconds = (analysisTimer.elapsedMilliseconds / 1000.0).toStringAsFixed(2);
124 125 126 127
      printStatus('$errorsMessage • analyzed $files, $seconds seconds');

      if (firstAnalysis && isBenchmarking) {
        writeBenchmark(analysisTimer, issueCount, -1); // TODO(ianh): track members missing dartdocs instead of saying -1
128
        server.dispose().whenComplete(() { exit(issueCount > 0 ? 1 : 0); });
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
      }

      firstAnalysis = false;
    }
  }

  bool _filterError(AnalysisError error) {
    // TODO(devoncarew): Also filter the regex items from `analyzeOnce()`.

    if (error.type == 'TODO')
      return true;

    return false;
  }

  void _handleAnalysisErrors(FileAnalysisErrors fileErrors) {
    fileErrors.errors.removeWhere(_filterError);

    analyzedPaths.add(fileErrors.file);
    analysisErrors[fileErrors.file] = fileErrors.errors;
  }
}

class AnalysisServer {
153
  AnalysisServer(this.sdk, this.directories);
154 155 156 157

  final String sdk;
  final List<String> directories;

158
  Process _process;
159 160
  final StreamController<bool> _analyzingController = new StreamController<bool>.broadcast();
  final StreamController<FileAnalysisErrors> _errorsController = new StreamController<FileAnalysisErrors>.broadcast();
161 162 163 164

  int _id = 0;

  Future<Null> start() async {
165 166
    final String snapshot = fs.path.join(sdk, 'bin/snapshots/analysis_server.dart.snapshot');
    final List<String> command = <String>[
167
      fs.path.join(dartSdkPath, 'bin', 'dart'),
168 169 170 171 172 173 174
      snapshot,
      '--sdk',
      sdk,
    ];

    printTrace('dart ${command.skip(1).join(' ')}');
    _process = await processManager.start(command);
175 176
    _process.exitCode.whenComplete(() => _process = null);

177
    final Stream<String> errorStream = _process.stderr.transform(UTF8.decoder).transform(const LineSplitter());
178
    errorStream.listen(printError);
179

180
    final Stream<String> inStream = _process.stdout.transform(UTF8.decoder).transform(const LineSplitter());
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
    inStream.listen(_handleServerResponse);

    // Available options (many of these are obsolete):
    //   enableAsync, enableDeferredLoading, enableEnums, enableNullAwareOperators,
    //   enableSuperMixins, generateDart2jsHints, generateHints, generateLints
    _sendCommand('analysis.updateOptions', <String, dynamic>{
      'options': <String, dynamic>{
        'enableSuperMixins': true
      }
    });

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

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

  Stream<bool> get onAnalyzing => _analyzingController.stream;
  Stream<FileAnalysisErrors> get onErrors => _errorsController.stream;

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

  void _sendCommand(String method, Map<String, dynamic> params) {
208
    final String message = JSON.encode(<String, dynamic> {
209 210 211 212 213 214 215 216 217 218 219
      'id': (++_id).toString(),
      'method': method,
      'params': params
    });
    _process.stdin.writeln(message);
    printTrace('==> $message');
  }

  void _handleServerResponse(String line) {
    printTrace('<== $line');

220
    final dynamic response = JSON.decode(line);
221 222 223

    if (response is Map<dynamic, dynamic>) {
      if (response['event'] != null) {
224 225
        final String event = response['event'];
        final dynamic params = response['params'];
226 227 228 229 230 231 232 233 234 235 236

        if (params is Map<dynamic, dynamic>) {
          if (event == 'server.status')
            _handleStatus(response['params']);
          else if (event == 'analysis.errors')
            _handleAnalysisIssues(response['params']);
          else if (event == 'server.error')
            _handleServerError(response['params']);
        }
      } else if (response['error'] != null) {
        // Fields are 'code', 'message', and 'stackTrace'.
237
        final Map<String, dynamic> error = response['error'];
238 239 240 241 242 243 244 245 246
        printError('Error response from the server: ${error['code']} ${error['message']}');
        if (error['stackTrace'] != null)
          printError(error['stackTrace']);
      }
    }
  }

  void _handleStatus(Map<String, dynamic> statusInfo) {
    // {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
Dan Rubel's avatar
Dan Rubel committed
247
    if (statusInfo['analysis'] != null && !_analyzingController.isClosed) {
248
      final bool isAnalyzing = statusInfo['analysis']['isAnalyzing'];
249 250 251 252 253 254 255 256 257 258 259 260 261
      _analyzingController.add(isAnalyzing);
    }
  }

  void _handleServerError(Map<String, dynamic> error) {
    // Fields are 'isFatal', 'message', and 'stackTrace'.
    printError('Error from the analysis server: ${error['message']}');
    if (error['stackTrace'] != null)
      printError(error['stackTrace']);
  }

  void _handleAnalysisIssues(Map<String, dynamic> issueInfo) {
    // {"event":"analysis.errors","params":{"file":"/Users/.../lib/main.dart","errors":[]}}
262 263
    final String file = issueInfo['file'];
    final List<AnalysisError> errors = issueInfo['errors'].map((Map<String, dynamic> json) => new AnalysisError(json)).toList();
264 265
    if (!_errorsController.isClosed)
      _errorsController.add(new FileAnalysisErrors(file, errors));
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
  }

  Future<bool> dispose() async {
    await _analyzingController.close();
    await _errorsController.close();
    return _process?.kill();
  }
}

class AnalysisError implements Comparable<AnalysisError> {
  AnalysisError(this.json);

  static final Map<String, int> _severityMap = <String, int> {
    'ERROR': 3,
    'WARNING': 2,
    'INFO': 1
  };

  // "severity":"INFO","type":"TODO","location":{
  //   "file":"/Users/.../lib/test.dart","offset":362,"length":72,"startLine":15,"startColumn":4
  // },"message":"...","hasFix":false}
  Map<String, dynamic> json;

  String get severity => json['severity'];
  int get severityLevel => _severityMap[severity] ?? 0;
  String get type => json['type'];
  String get message => json['message'];
  String get code => json['code'];

  String get file => json['location']['file'];
  int get startLine => json['location']['startLine'];
  int get startColumn => json['location']['startColumn'];
  int get offset => json['location']['offset'];

  @override
  int compareTo(AnalysisError other) {
    // Sort in order of file path, error location, severity, and message.
    if (file != other.file)
      return file.compareTo(other.file);

    if (offset != other.offset)
      return offset - other.offset;

309
    final int diff = other.severityLevel - severityLevel;
310 311 312 313 314 315 316 317
    if (diff != 0)
      return diff;

    return message.compareTo(other.message);
  }

  @override
  String toString() {
318
    final String relativePath = fs.path.relative(file);
319 320 321 322 323 324 325 326 327 328 329 330 331 332
    return '${severity.toLowerCase().padLeft(7)}$message$relativePath:$startLine:$startColumn';
  }

  String toLegacyString() {
    return '[${severity.toLowerCase()}] $message ($file:$startLine:$startColumn)';
  }
}

class FileAnalysisErrors {
  FileAnalysisErrors(this.file, this.errors);

  final String file;
  final List<AnalysisError> errors;
}