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();
+}