dartfix stub implementation

This simple first step cmdline utility launches and manages an external analysis server process.
Future CLs will add dartfix specific functionality.

Change-Id: Iba32177acd8ca1edd703bad78e55cd1e88edb6bd
Reviewed-on: https://dart-review.googlesource.com/76320
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Dan Rubel <danrubel@google.com>
diff --git a/pkg/analyzer_cli/bin/fix.dart b/pkg/analyzer_cli/bin/fix.dart
new file mode 100644
index 0000000..2ef38e4
--- /dev/null
+++ b/pkg/analyzer_cli/bin/fix.dart
@@ -0,0 +1,14 @@
+#!/usr/bin/env dart
+// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analyzer_cli/src/fix/driver.dart';
+
+/// The entry point for dartfix.
+main(List<String> args) async {
+  Driver starter = new Driver();
+
+  // Wait for the starter to complete.
+  await starter.start(args);
+}
diff --git a/pkg/analyzer_cli/lib/src/fix/driver.dart b/pkg/analyzer_cli/lib/src/fix/driver.dart
new file mode 100644
index 0000000..5bbf033
--- /dev/null
+++ b/pkg/analyzer_cli/lib/src/fix/driver.dart
@@ -0,0 +1,209 @@
+import 'dart:async';
+
+import 'package:analyzer_cli/src/fix/options.dart';
+import 'package:analyzer_cli/src/fix/server.dart';
+import 'package:analysis_server/src/protocol/protocol_internal.dart';
+import 'package:analysis_server/protocol/protocol_generated.dart';
+import 'package:analyzer_plugin/protocol/protocol_common.dart';
+
+class Driver {
+  static const timeout = const Duration(seconds: 5);
+
+  final Server server = new Server();
+
+  Completer serverConnected;
+  Completer analysisComplete;
+  bool verbose;
+
+  Future start(List<String> args) async {
+    final options = Options.parse(args);
+
+    /// Only happens in testing.
+    if (options == null) {
+      return null;
+    }
+    verbose = options.verbose;
+
+    serverConnected = new Completer();
+    analysisComplete = new Completer();
+
+    await startServer(options);
+    outSink.writeln('Analyzing...');
+    await setupAnalysis(options);
+
+    // TODO(danrubel): Request fixes rather than waiting for analysis complete
+    await analysisComplete.future;
+
+    outSink.writeln('Analysis complete.');
+    await stopServer(server);
+  }
+
+  Future startServer(Options options) async {
+    if (options.verbose) {
+      server.debugStdio();
+    }
+    verboseOut('Starting...');
+    await server.start(sdkPath: options.sdkPath);
+    server.listenToOutput(dispatchNotification);
+    return serverConnected.future.timeout(timeout, onTimeout: () {
+      printAndFail('Failed to connect to server');
+    });
+  }
+
+  Future setupAnalysis(Options options) async {
+    verboseOut('Setup analysis');
+
+    await server.send("server.setSubscriptions",
+        new ServerSetSubscriptionsParams([ServerService.STATUS]).toJson());
+
+    await server.send(
+        "analysis.setAnalysisRoots",
+        new AnalysisSetAnalysisRootsParams(
+          options.analysisRoots,
+          const [],
+        ).toJson());
+  }
+
+  Future stopServer(Server server) async {
+    verboseOut('Stopping...');
+    await server.send("server.shutdown", null);
+    await server.exitCode.timeout(const Duration(seconds: 5), onTimeout: () {
+      return server.kill('server failed to exit');
+    });
+  }
+
+  /**
+   * Dispatch the notification named [event], and containing parameters
+   * [params], to the appropriate stream.
+   */
+  void dispatchNotification(String event, params) {
+    ResponseDecoder decoder = new ResponseDecoder(null);
+    switch (event) {
+      case "server.connected":
+        onServerConnected(
+            new ServerConnectedParams.fromJson(decoder, 'params', params));
+        break;
+//      case "server.error":
+//        outOfTestExpect(params, isServerErrorParams);
+//        _onServerError
+//            .add(new ServerErrorParams.fromJson(decoder, 'params', params));
+//        break;
+      case "server.status":
+        onServerStatus(
+            new ServerStatusParams.fromJson(decoder, 'params', params));
+        break;
+//      case "analysis.analyzedFiles":
+//        outOfTestExpect(params, isAnalysisAnalyzedFilesParams);
+//        _onAnalysisAnalyzedFiles.add(new AnalysisAnalyzedFilesParams.fromJson(
+//            decoder, 'params', params));
+//        break;
+//      case "analysis.closingLabels":
+//        outOfTestExpect(params, isAnalysisClosingLabelsParams);
+//        _onAnalysisClosingLabels.add(new AnalysisClosingLabelsParams.fromJson(
+//            decoder, 'params', params));
+//        break;
+      case "analysis.errors":
+        onAnalysisErrors(
+            new AnalysisErrorsParams.fromJson(decoder, 'params', params));
+        break;
+//      case "analysis.flushResults":
+//        outOfTestExpect(params, isAnalysisFlushResultsParams);
+//        _onAnalysisFlushResults.add(
+//            new AnalysisFlushResultsParams.fromJson(decoder, 'params', params));
+//        break;
+//      case "analysis.folding":
+//        outOfTestExpect(params, isAnalysisFoldingParams);
+//        _onAnalysisFolding
+//            .add(new AnalysisFoldingParams.fromJson(decoder, 'params', params));
+//        break;
+//      case "analysis.highlights":
+//        outOfTestExpect(params, isAnalysisHighlightsParams);
+//        _onAnalysisHighlights.add(
+//            new AnalysisHighlightsParams.fromJson(decoder, 'params', params));
+//        break;
+//      case "analysis.implemented":
+//        outOfTestExpect(params, isAnalysisImplementedParams);
+//        _onAnalysisImplemented.add(
+//            new AnalysisImplementedParams.fromJson(decoder, 'params', params));
+//        break;
+//      case "analysis.invalidate":
+//        outOfTestExpect(params, isAnalysisInvalidateParams);
+//        _onAnalysisInvalidate.add(
+//            new AnalysisInvalidateParams.fromJson(decoder, 'params', params));
+//        break;
+//      case "analysis.navigation":
+//        outOfTestExpect(params, isAnalysisNavigationParams);
+//        _onAnalysisNavigation.add(
+//            new AnalysisNavigationParams.fromJson(decoder, 'params', params));
+//        break;
+//      case "analysis.occurrences":
+//        outOfTestExpect(params, isAnalysisOccurrencesParams);
+//        _onAnalysisOccurrences.add(
+//            new AnalysisOccurrencesParams.fromJson(decoder, 'params', params));
+//        break;
+//      case "analysis.outline":
+//        outOfTestExpect(params, isAnalysisOutlineParams);
+//        _onAnalysisOutline
+//            .add(new AnalysisOutlineParams.fromJson(decoder, 'params', params));
+//        break;
+//      case "analysis.overrides":
+//        outOfTestExpect(params, isAnalysisOverridesParams);
+//        _onAnalysisOverrides.add(
+//            new AnalysisOverridesParams.fromJson(decoder, 'params', params));
+//        break;
+//      case "completion.results":
+//        outOfTestExpect(params, isCompletionResultsParams);
+//        _onCompletionResults.add(
+//            new CompletionResultsParams.fromJson(decoder, 'params', params));
+//        break;
+//      case "search.results":
+//        outOfTestExpect(params, isSearchResultsParams);
+//        _onSearchResults
+//            .add(new SearchResultsParams.fromJson(decoder, 'params', params));
+//        break;
+//      case "execution.launchData":
+//        outOfTestExpect(params, isExecutionLaunchDataParams);
+//        _onExecutionLaunchData.add(
+//            new ExecutionLaunchDataParams.fromJson(decoder, 'params', params));
+//        break;
+//      case "flutter.outline":
+//        outOfTestExpect(params, isFlutterOutlineParams);
+//        _onFlutterOutline
+//            .add(new FlutterOutlineParams.fromJson(decoder, 'params', params));
+//        break;
+//      default:
+//        printAndFail('Unexpected notification: $event');
+//        break;
+    }
+  }
+
+  void onAnalysisErrors(AnalysisErrorsParams params) {
+    List<AnalysisError> errors = params.errors;
+    if (errors.isNotEmpty) {
+      outSink.writeln(params.file);
+      for (AnalysisError error in errors) {
+        Location loc = error.location;
+        outSink.writeln('  ${error.message}'
+            ' at ${loc.startLine}:${loc.startColumn}');
+      }
+    }
+  }
+
+  void onServerConnected(ServerConnectedParams params) {
+    verboseOut('Connected to server');
+    serverConnected.complete();
+  }
+
+  void onServerStatus(ServerStatusParams params) {
+    if (params.analysis != null && !params.analysis.isAnalyzing) {
+      verboseOut('Analysis complete');
+      analysisComplete.complete();
+    }
+  }
+
+  void verboseOut(String message) {
+    if (verbose) {
+      outSink.writeln(message);
+    }
+  }
+}
diff --git a/pkg/analyzer_cli/lib/src/fix/options.dart b/pkg/analyzer_cli/lib/src/fix/options.dart
new file mode 100644
index 0000000..802eac1
--- /dev/null
+++ b/pkg/analyzer_cli/lib/src/fix/options.dart
@@ -0,0 +1,107 @@
+import 'dart:io';
+
+import 'package:analyzer/src/util/sdk.dart';
+import 'package:args/args.dart';
+import 'package:meta/meta.dart';
+
+@visibleForTesting
+StringSink errorSink = stderr;
+
+@visibleForTesting
+StringSink outSink = stdout;
+
+@visibleForTesting
+ExitHandler exitHandler = exit;
+
+@visibleForTesting
+typedef void ExitHandler(int code);
+
+/// Command line options for `dartfix`.
+class Options {
+  String sdkPath;
+  List<String> analysisRoots;
+  bool verbose;
+
+  static Options parse(List<String> args,
+      {printAndFail(String msg) = printAndFail}) {
+    final parser = new ArgParser(allowTrailingOptions: true);
+
+    parser
+      ..addOption(_sdkPathOption, help: 'The path to the Dart SDK.')
+      ..addFlag(_helpOption,
+          abbr: 'h',
+          help:
+              'Display this help message. Add --verbose to show hidden options.',
+          defaultsTo: false,
+          negatable: false)
+      ..addFlag(_verboseOption,
+          abbr: 'v',
+          defaultsTo: false,
+          help: 'Verbose output.',
+          negatable: false);
+
+    ArgResults results;
+    try {
+      results = parser.parse(args);
+    } on FormatException catch (e) {
+      errorSink.writeln(e.message);
+      _showUsage(parser);
+      exitHandler(15);
+      return null; // Only reachable in testing.
+    }
+
+    if (results[_helpOption] as bool) {
+      _showUsage(parser);
+      exitHandler(0);
+      return null; // Only reachable in testing.
+    }
+
+    Options options = new Options._fromArgs(results);
+
+    // Check Dart SDK, and infer if unspecified.
+    options.sdkPath ??= getSdkPath(args);
+    String sdkPath = options.sdkPath;
+    if (sdkPath == null) {
+      errorSink.writeln('No Dart SDK found.');
+      _showUsage(parser);
+      return null; // Only reachable in testing.
+    }
+    if (!(new Directory(sdkPath)).existsSync()) {
+      printAndFail('Invalid Dart SDK path: $sdkPath');
+      return null; // Only reachable in testing.
+    }
+
+    // Check for files and/or directories to analyze.
+    if (options.analysisRoots == null || options.analysisRoots.isEmpty) {
+      errorSink.writeln('Expected at least one file or directory to analyze.');
+      _showUsage(parser);
+      exitHandler(15);
+      return null; // Only reachable in testing.
+    }
+
+    return options;
+  }
+
+  Options._fromArgs(ArgResults results)
+      : analysisRoots = results.rest,
+        sdkPath = results[_sdkPathOption] as String,
+        verbose = results[_verboseOption] as bool;
+
+  static _showUsage(ArgParser parser) {
+    errorSink.writeln(
+        'Usage: $_binaryName [options...] <directory or list of files>');
+    errorSink.writeln('');
+    errorSink.writeln(parser.usage);
+  }
+}
+
+const _binaryName = 'dartfix';
+const _helpOption = 'help';
+const _sdkPathOption = 'dart-sdk';
+const _verboseOption = 'verbose';
+
+/// Print the given [message] to stderr and exit with the given [exitCode].
+void printAndFail(String message, {int exitCode: 15}) {
+  errorSink.writeln(message);
+  exitHandler(exitCode);
+}
diff --git a/pkg/analyzer_cli/lib/src/fix/server.dart b/pkg/analyzer_cli/lib/src/fix/server.dart
new file mode 100644
index 0000000..31467ab
--- /dev/null
+++ b/pkg/analyzer_cli/lib/src/fix/server.dart
@@ -0,0 +1,342 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart';
+
+/**
+ * Type of callbacks used to process notifications.
+ */
+typedef void NotificationProcessor(String event, params);
+
+/**
+ * Instances of the class [Server] manage a connection to a server process, and
+ * facilitate communication to and from the server.
+ */
+class Server {
+  /**
+   * Server process object, or null if server hasn't been started yet.
+   */
+  Process _process;
+
+  /**
+   * Commands that have been sent to the server but not yet acknowledged, and
+   * the [Completer] objects which should be completed when acknowledgement is
+   * received.
+   */
+  final Map<String, Completer<Map<String, dynamic>>> _pendingCommands =
+      <String, Completer<Map<String, dynamic>>>{};
+
+  /**
+   * Number which should be used to compute the 'id' to send in the next command
+   * sent to the server.
+   */
+  int _nextId = 0;
+
+  /**
+   * Messages which have been exchanged with the server; we buffer these
+   * up until the test finishes, so that they can be examined in the debugger
+   * or printed out in response to a call to [debugStdio].
+   */
+  final List<String> _recordedStdio = <String>[];
+
+  /**
+   * True if we are currently printing out messages exchanged with the server.
+   */
+  bool _debuggingStdio = false;
+
+  /**
+   * True if we've received bad data from the server, and we are aborting the
+   * test.
+   */
+  bool _receivedBadDataFromServer = false;
+
+  /**
+   * Stopwatch that we use to generate timing information for debug output.
+   */
+  Stopwatch _time = new Stopwatch();
+
+  /**
+   * The [currentElapseTime] at which the last communication was received from the server
+   * or `null` if no communication has been received.
+   */
+  double lastCommunicationTime;
+
+  /**
+   * The current elapse time (seconds) since the server was started.
+   */
+  double get currentElapseTime => _time.elapsedTicks / _time.frequency;
+
+  /**
+   * Future that completes when the server process exits.
+   */
+  Future<int> get exitCode => _process.exitCode;
+
+  /**
+   * Print out any messages exchanged with the server.  If some messages have
+   * already been exchanged with the server, they are printed out immediately.
+   */
+  void debugStdio() {
+    if (_debuggingStdio) {
+      return;
+    }
+    _debuggingStdio = true;
+    for (String line in _recordedStdio) {
+      print(line);
+    }
+  }
+
+  /**
+   * Find the root directory of the analysis_server package by proceeding
+   * upward to the 'test' dir, and then going up one more directory.
+   */
+  String findRoot(String pathname) {
+    while (!['benchmark', 'test'].contains(basename(pathname))) {
+      String parent = dirname(pathname);
+      if (parent.length >= pathname.length) {
+        throw new Exception("Can't find root directory");
+      }
+      pathname = parent;
+    }
+    return dirname(pathname);
+  }
+
+  /**
+   * Return a future that will complete when all commands that have been sent
+   * to the server so far have been flushed to the OS buffer.
+   */
+  Future flushCommands() {
+    return _process.stdin.flush();
+  }
+
+  /**
+   * Stop the server.
+   */
+  Future<int> kill(String reason) {
+    debugStdio();
+    _recordStdio('FORCIBLY TERMINATING PROCESS: $reason');
+    _process.kill();
+    return _process.exitCode;
+  }
+
+  /**
+   * Start listening to output from the server, and deliver notifications to
+   * [notificationProcessor].
+   */
+  void listenToOutput(NotificationProcessor notificationProcessor) {
+    _process.stdout
+        .transform(utf8.decoder)
+        .transform(new LineSplitter())
+        .listen((String line) {
+      lastCommunicationTime = currentElapseTime;
+      String trimmedLine = line.trim();
+
+      // Guard against lines like:
+      //   {"event":"server.connected","params":{...}}Observatory listening on ...
+      final String observatoryMessage = 'Observatory listening on ';
+      if (trimmedLine.contains(observatoryMessage)) {
+        trimmedLine = trimmedLine
+            .substring(0, trimmedLine.indexOf(observatoryMessage))
+            .trim();
+      }
+      if (trimmedLine.isEmpty) {
+        return;
+      }
+
+      _recordStdio('<== $trimmedLine');
+      var message;
+      try {
+        message = json.decoder.convert(trimmedLine);
+      } catch (exception) {
+        _badDataFromServer('JSON decode failure: $exception');
+        return;
+      }
+      Map messageAsMap = message;
+      if (messageAsMap.containsKey('id')) {
+        String id = message['id'];
+        Completer<Map<String, dynamic>> completer = _pendingCommands[id];
+        if (completer == null) {
+          throw 'Unexpected response from server: id=$id';
+        } else {
+          _pendingCommands.remove(id);
+        }
+        if (messageAsMap.containsKey('error')) {
+          completer.completeError(new ServerErrorMessage(messageAsMap));
+        } else {
+          Map<String, dynamic> result = messageAsMap['result'];
+          completer.complete(result);
+        }
+      } else {
+        String event = messageAsMap['event'];
+        notificationProcessor(event, messageAsMap['params']);
+      }
+    });
+    _process.stderr
+        .transform((new Utf8Codec()).decoder)
+        .transform(new LineSplitter())
+        .listen((String line) {
+      String trimmedLine = line.trim();
+      _recordStdio('ERR:  $trimmedLine');
+      _badDataFromServer('Message received on stderr', silent: true);
+    });
+  }
+
+  /**
+   * Send a command to the server.  An 'id' will be automatically assigned.
+   * The returned [Future] will be completed when the server acknowledges the
+   * command with a response.  If the server acknowledges the command with a
+   * normal (non-error) response, the future will be completed with the 'result'
+   * field from the response.  If the server acknowledges the command with an
+   * error response, the future will be completed with an error.
+   */
+  Future<Map<String, dynamic>> send(
+      String method, Map<String, dynamic> params) {
+    String id = '${_nextId++}';
+    Map<String, dynamic> command = <String, dynamic>{
+      'id': id,
+      'method': method
+    };
+    if (params != null) {
+      command['params'] = params;
+    }
+    Completer<Map<String, dynamic>> completer =
+        new Completer<Map<String, dynamic>>();
+    _pendingCommands[id] = completer;
+    String line = json.encode(command);
+    _recordStdio('==> $line');
+    _process.stdin.add(utf8.encoder.convert("$line\n"));
+    return completer.future;
+  }
+
+  /**
+   * Start the server. If [profileServer] is `true`, the server will be started
+   * with "--observe" and "--pause-isolates-on-exit", allowing the observatory
+   * to be used.
+   */
+  Future start({
+    int diagnosticPort,
+    String instrumentationLogFile,
+    bool profileServer: false,
+    String sdkPath,
+    int servicesPort,
+    bool useAnalysisHighlight2: false,
+  }) async {
+    if (_process != null) {
+      throw new Exception('Process already started');
+    }
+    _time.start();
+    String dartBinary = Platform.executable;
+
+    // The integration tests run 3x faster when run from snapshots (you need to
+    // run test.py with --use-sdk).
+    final bool useSnapshot = true;
+    String serverPath;
+
+    if (useSnapshot) {
+      // Look for snapshots/analysis_server.dart.snapshot.
+      serverPath = normalize(join(dirname(Platform.resolvedExecutable),
+          'snapshots', 'analysis_server.dart.snapshot'));
+
+      if (!FileSystemEntity.isFileSync(serverPath)) {
+        // Look for dart-sdk/bin/snapshots/analysis_server.dart.snapshot.
+        serverPath = normalize(join(dirname(Platform.resolvedExecutable),
+            'dart-sdk', 'bin', 'snapshots', 'analysis_server.dart.snapshot'));
+      }
+    } else {
+      String rootDir =
+          findRoot(Platform.script.toFilePath(windows: Platform.isWindows));
+      serverPath = normalize(join(rootDir, 'bin', 'server.dart'));
+    }
+
+    List<String> arguments = [];
+    //
+    // Add VM arguments.
+    //
+    if (profileServer) {
+      if (servicesPort == null) {
+        arguments.add('--observe');
+      } else {
+        arguments.add('--observe=$servicesPort');
+      }
+      arguments.add('--pause-isolates-on-exit');
+    } else if (servicesPort != null) {
+      arguments.add('--enable-vm-service=$servicesPort');
+    }
+    if (Platform.packageConfig != null) {
+      arguments.add('--packages=${Platform.packageConfig}');
+    }
+    //
+    // Add the server executable.
+    //
+    arguments.add(serverPath);
+    //
+    // Add server arguments.
+    //
+    arguments.add('--suppress-analytics');
+    if (diagnosticPort != null) {
+      arguments.add('--port');
+      arguments.add(diagnosticPort.toString());
+    }
+    if (instrumentationLogFile != null) {
+      arguments.add('--instrumentation-log-file=$instrumentationLogFile');
+    }
+    if (sdkPath != null) {
+      arguments.add('--sdk=$sdkPath');
+    }
+    if (useAnalysisHighlight2) {
+      arguments.add('--useAnalysisHighlight2');
+    }
+    _process = await Process.start(dartBinary, arguments);
+    _process.exitCode.then((int code) {
+      if (code != 0) {
+        _badDataFromServer('server terminated with exit code $code');
+      }
+    });
+  }
+
+  /**
+   * Deal with bad data received from the server.
+   */
+  void _badDataFromServer(String details, {bool silent: false}) {
+    if (!silent) {
+      _recordStdio('BAD DATA FROM SERVER: $details');
+    }
+    if (_receivedBadDataFromServer) {
+      // We're already dealing with it.
+      return;
+    }
+    _receivedBadDataFromServer = true;
+    debugStdio();
+    // Give the server 1 second to continue outputting bad data
+    // such as outputting a stacktrace.
+    new Future.delayed(new Duration(seconds: 1), () {
+      throw 'Bad data received from server: $details';
+    });
+  }
+
+  /**
+   * Record a message that was exchanged with the server, and print it out if
+   * [debugStdio] has been called.
+   */
+  void _recordStdio(String line) {
+    double elapsedTime = currentElapseTime;
+    line = "$elapsedTime: $line";
+    if (_debuggingStdio) {
+      print(line);
+    }
+    _recordedStdio.add(line);
+  }
+}
+
+/**
+ * An error result from a server request.
+ */
+class ServerErrorMessage {
+  final Map message;
+
+  ServerErrorMessage(this.message);
+
+  dynamic get error => message['error'];
+
+  String toString() => message.toString();
+}