Improve dartfix console output

Change-Id: Ifa55c6d03487a987cf6b0a4a6ae58d6068e54d8b
Reviewed-on: https://dart-review.googlesource.com/c/80180
Commit-Queue: Dan Rubel <danrubel@google.com>
Reviewed-by: Devon Carew <devoncarew@google.com>
This commit is contained in:
danrubel 2018-10-16 21:25:45 +00:00 committed by commit-bot@chromium.org
parent da67d58110
commit 6066ac83e3
5 changed files with 149 additions and 190 deletions

View file

@ -6,10 +6,6 @@ import 'dart:io' as io;
/// The context for dartfix.
class Context {
StringSink get stdout => io.stdout;
StringSink get stderr => io.stderr;
String get workingDir => io.Directory.current.path;
bool exists(String filePath) =>
@ -23,8 +19,4 @@ class Context {
bool isDirectory(String filePath) =>
io.FileSystemEntity.typeSync(filePath) ==
io.FileSystemEntityType.directory;
void print([String text = '']) {
stdout.writeln(text);
}
}

View file

@ -19,32 +19,26 @@ import 'package:path/path.dart' as path;
const runAnalysisServerFromSource = false;
class Driver {
static const progressThreshold = 10;
final Context context = new Context();
Context context;
Logger logger;
Server server;
Completer serverConnected;
Completer analysisComplete;
bool force;
bool overwrite;
bool verbose;
List<String> targets;
Logger logger;
int progressCount = progressThreshold;
Ansi get ansi => logger.ansi;
Future start(List<String> args) async {
final Options options = Options.parse(args, context);
final Options options = Options.parse(args);
force = options.force;
overwrite = options.overwrite;
verbose = options.verbose;
targets = options.targets;
context = options.context;
logger = options.logger;
EditDartfixResult result;
@ -53,8 +47,8 @@ class Driver {
bool normalShutdown = false;
try {
await setupAnalysis(options);
result = await requestFixes(options);
final progress = await setupAnalysis(options);
result = await requestFixes(options, progress);
normalShutdown = true;
} finally {
try {
@ -82,14 +76,15 @@ class Driver {
sdkPath: options.sdkPath, useSnapshot: !runAnalysisServerFromSource);
server.listenToOutput(dispatchNotification);
return serverConnected.future.timeout(connectTimeout, onTimeout: () {
context.stderr.writeln('Failed to connect to server');
logger.stderr('Failed to connect to server');
context.exit(15);
});
}
Future setupAnalysis(Options options) async {
context.stdout.write('${ansi.emphasized('Calculating fixes')}...');
logger.trace('\nSetup analysis');
Future<Progress> setupAnalysis(Options options) async {
final progress = logger.progress('${ansi.emphasized('Calculating fixes')}');
logger.trace('');
logger.trace('Setup analysis');
await server.send(SERVER_REQUEST_SET_SUBSCRIPTIONS,
new ServerSetSubscriptionsParams([ServerService.STATUS]).toJson());
await server.send(
@ -98,15 +93,17 @@ class Driver {
options.targets,
const [],
).toJson());
return progress;
}
Future<EditDartfixResult> requestFixes(Options options) async {
Future<EditDartfixResult> requestFixes(
Options options, Progress progress) async {
logger.trace('Requesting fixes');
analysisComplete = new Completer();
Map<String, dynamic> json = await server.send(
EDIT_REQUEST_DARTFIX, new EditDartfixParams(options.targets).toJson());
await analysisComplete?.future;
resetProgress();
progress.finish(showTiming: true);
ResponseDecoder decoder = new ResponseDecoder(null);
return EditDartfixResult.fromJson(decoder, 'result', json);
}
@ -130,16 +127,16 @@ class Driver {
result.otherRecommendations,
);
if (result.descriptionOfFixes.isEmpty) {
context.print('');
context.print(result.otherRecommendations.isNotEmpty
logger.stdout('');
logger.stdout(result.otherRecommendations.isNotEmpty
? 'No recommended changes that cannot be automatically applied.'
: 'No recommended changes.');
return;
}
context.print('');
context.print(ansi.emphasized('Files to be changed:'));
logger.stdout('');
logger.stdout(ansi.emphasized('Files to be changed:'));
for (SourceFileEdit fileEdit in result.fixes) {
context.print(' ${_relativePath(fileEdit.file)}');
logger.stdout(' ${_relativePath(fileEdit.file)}');
}
if (shouldApplyChanges(result)) {
for (SourceFileEdit fileEdit in result.fixes) {
@ -150,33 +147,33 @@ class Driver {
}
await file.writeAsString(code);
}
context.print('Changes applied.');
logger.stdout('Changes applied.');
}
}
void showDescriptions(String title, List<String> descriptions) {
if (descriptions.isNotEmpty) {
context.print('');
context.print(ansi.emphasized('$title:'));
logger.stdout('');
logger.stdout(ansi.emphasized('$title:'));
List<String> sorted = new List.from(descriptions)..sort();
for (String line in sorted) {
context.print(' $line');
logger.stdout(' $line');
}
}
}
bool shouldApplyChanges(EditDartfixResult result) {
context.print();
logger.stdout('');
if (result.hasErrors) {
context.print('WARNING: The analyzed source contains errors'
logger.stdout('WARNING: The analyzed source contains errors'
' that may affect the accuracy of these changes.');
context.print();
logger.stdout('');
if (!force) {
context.print('Rerun with --$forceOption to apply these changes.');
logger.stdout('Rerun with --$forceOption to apply these changes.');
return false;
}
} else if (!overwrite && !force) {
context.print('Rerun with --$overwriteOption to apply these changes.');
logger.stdout('Rerun with --$overwriteOption to apply these changes.');
return false;
}
return true;
@ -292,18 +289,14 @@ class Driver {
if (!shouldFilterError(error)) {
if (!foundAtLeastOneError) {
foundAtLeastOneError = true;
resetProgress();
context.print('${_relativePath(params.file)}:');
logger.stdout('${_relativePath(params.file)}:');
}
Location loc = error.location;
context.print(' ${_toSentenceFragment(error.message)}'
logger.stdout(' ${_toSentenceFragment(error.message)}'
'${loc.startLine}:${loc.startColumn}');
}
}
}
if (!foundAtLeastOneError) {
showProgress();
}
}
void onServerConnected(ServerConnectedParams params) {
@ -321,7 +314,7 @@ class Driver {
if (params.stackTrace != null) {
message.writeln(params.stackTrace);
}
context.stderr.writeln(message.toString());
logger.stderr(message.toString());
context.exit(15);
}
@ -333,13 +326,6 @@ class Driver {
}
}
void resetProgress() {
if (!verbose && progressCount >= progressThreshold) {
context.print();
}
progressCount = 0;
}
bool shouldFilterError(AnalysisError error) {
// Do not show TODOs or errors that will be automatically fixed.
@ -350,13 +336,6 @@ class Driver {
error.code == 'wrong_number_of_type_arguments_constructor';
}
void showProgress() {
if (!verbose && progressCount % progressThreshold == 0) {
context.stdout.write('.');
}
++progressCount;
}
bool isTarget(String filePath) {
for (String target in targets) {
if (filePath == target || path.isWithin(target, filePath)) {

View file

@ -11,23 +11,17 @@ import 'package:path/path.dart' as path;
/// Command line options for `dartfix`.
class Options {
final Context context;
List<String> targets;
bool force;
bool overwrite;
String sdkPath;
bool verbose;
bool useColor;
Logger logger;
Options(this.context);
List<String> targets;
String sdkPath;
final bool force;
final bool overwrite;
final bool verbose;
final bool useColor;
static Options parse(List<String> args, Context context, {Logger logger}) {
Options options = new Options(context ?? new Context());
final parser = new ArgParser(allowTrailingOptions: true);
parser
static Options parse(List<String> args, {Context context, Logger logger}) {
final parser = new ArgParser(allowTrailingOptions: true)
..addFlag(overwriteOption,
abbr: 'w',
help: 'Overwrite files with the recommended changes.',
@ -52,56 +46,19 @@ class Options {
help: 'Use ansi colors when printing messages.',
defaultsTo: Ansi.terminalSupportsAnsi);
context ??= new Context();
ArgResults results;
try {
results = parser.parse(args);
} on FormatException catch (e) {
context.stderr.writeln(e.message);
_showUsage(parser, context);
logger ??= new Logger.standard(ansi: new Ansi(Ansi.terminalSupportsAnsi));
logger.stderr(e.message);
_showUsage(parser, logger);
context.exit(15);
}
if (results[_helpOption] as bool) {
_showUsage(parser, context);
context.exit(1);
}
options._fromArgs(results);
// Infer the Dart SDK location
options.sdkPath = getSdkPath(args);
String sdkPath = options.sdkPath;
if (sdkPath == null) {
context.stderr.writeln('No Dart SDK found.');
_showUsage(parser, context);
}
if (!context.exists(sdkPath)) {
context.stderr.writeln('Invalid Dart SDK path: $sdkPath');
context.exit(15);
}
// Check for files and/or directories to analyze.
if (options.targets == null || options.targets.isEmpty) {
context.stderr
.writeln('Expected at least one file or directory to analyze.');
_showUsage(parser, context);
context.exit(15);
}
// Normalize and verify paths
options.targets =
options.targets.map<String>(options.makeAbsoluteAndNormalize).toList();
for (String target in options.targets) {
if (!context.isDirectory(target)) {
if (!context.exists(target)) {
context.stderr.writeln('Target does not exist: $target');
} else {
context.stderr.writeln('Expected directory, but found: $target');
}
_showUsage(parser, context);
context.exit(15);
}
}
Options options = new Options._fromArgs(context, results);
if (logger == null) {
if (options.verbose) {
@ -115,26 +72,63 @@ class Options {
));
}
}
options.logger = logger;
if (results[_helpOption] as bool) {
_showUsage(parser, logger);
context.exit(1);
}
// Infer the Dart SDK location
options.sdkPath = getSdkPath(args);
String sdkPath = options.sdkPath;
if (sdkPath == null) {
logger.stderr('No Dart SDK found.');
_showUsage(parser, logger);
}
if (!context.exists(sdkPath)) {
logger.stderr('Invalid Dart SDK path: $sdkPath');
context.exit(15);
}
// Check for files and/or directories to analyze.
if (options.targets == null || options.targets.isEmpty) {
logger.stderr('Expected at least one file or directory to analyze.');
_showUsage(parser, logger);
context.exit(15);
}
// Normalize and verify paths
options.targets =
options.targets.map<String>(options.makeAbsoluteAndNormalize).toList();
for (String target in options.targets) {
if (!context.isDirectory(target)) {
if (!context.exists(target)) {
logger.stderr('Target does not exist: $target');
} else {
logger.stderr('Expected directory, but found: $target');
}
_showUsage(parser, logger);
context.exit(15);
}
}
if (options.verbose) {
context.print('Targets:');
logger.trace('Targets:');
for (String target in options.targets) {
context.print(' $target');
logger.trace(' $target');
}
}
return options;
}
void _fromArgs(ArgResults results) {
targets = results.rest;
force = results[forceOption] as bool;
overwrite = results[overwriteOption] as bool;
verbose = results[_verboseOption] as bool;
useColor = results.wasParsed('color') ? results['color'] as bool : null;
}
Options._fromArgs(this.context, ArgResults results)
: targets = results.rest,
force = results[forceOption] as bool,
overwrite = results[overwriteOption] as bool,
verbose = results[_verboseOption] as bool,
useColor = results.wasParsed('color') ? results['color'] as bool : null;
String makeAbsoluteAndNormalize(String target) {
if (!path.isAbsolute(target)) {
@ -143,11 +137,10 @@ class Options {
return path.normalize(target);
}
static _showUsage(ArgParser parser, Context context) {
context.stderr
.writeln('Usage: $_binaryName [options...] <directory paths>');
context.stderr.writeln('');
context.stderr.writeln(parser.usage);
static _showUsage(ArgParser parser, Logger logger) {
logger.stderr('Usage: $_binaryName [options...] <directory paths>');
logger.stderr('');
logger.stderr(parser.usage);
}
}

View file

@ -3,7 +3,6 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer_cli/src/fix/options.dart';
import 'package:cli_util/cli_logging.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
@ -12,7 +11,12 @@ import 'test_context.dart';
main() {
group('Options', () {
TestContext context;
Logger logger;
TestLogger logger;
setUp(() {
context = new TestContext();
logger = new TestLogger();
});
String p(String filePath) => context.convertPath(filePath);
@ -29,13 +33,13 @@ main() {
Options options;
int actualExitCode;
try {
options = Options.parse(args, context, logger: logger);
options = Options.parse(args, context: context, logger: logger);
} on TestExit catch (e) {
actualExitCode = e.code;
}
expect(context.stderr.toString(),
expect(logger.stderrBuffer.toString(),
errorOut != null ? contains(errorOut) : isEmpty);
expect(context.stdout.toString(),
expect(logger.stdoutBuffer.toString(),
normalOut != null ? contains(normalOut) : isEmpty);
if (exitCode != null) {
expect(actualExitCode, exitCode, reason: 'exit code');
@ -59,11 +63,6 @@ main() {
return options;
}
setUp(() {
context = new TestContext();
logger = new _TestLogger(context);
});
test('force', () {
parse(['--force', 'foo'], force: true, targetSuffixes: ['foo']);
});
@ -77,6 +76,15 @@ main() {
errorOut: 'Could not find an option named "foo"', exitCode: 15);
});
test('invalid option no logger', () {
try {
Options.parse(['--foo'], context: context);
fail('Expected exception');
} on TestExit catch (e) {
expect(e.code, 15, reason: 'exit code');
}
});
test('invalid target', () {
parse(['foo.dart'],
errorOut: 'Expected directory, but found', exitCode: 15);
@ -96,7 +104,7 @@ main() {
});
test('verbose', () {
parse(['--verbose', 'foo'], verbose: true, normalOut: 'Targets:');
parse(['--verbose', 'foo'], verbose: true);
});
});
}
@ -115,34 +123,3 @@ void expectContains(Iterable<String> collection, String suffix) {
}
fail('Expected one of $collection\n to end with "$suffix"');
}
class _TestLogger implements Logger {
final TestContext context;
final Ansi ansi;
_TestLogger(this.context) : this.ansi = new Ansi(false);
@override
void flush() {}
@override
bool get isVerbose => false;
@override
Progress progress(String message) {
return new SimpleProgress(this, message);
}
@override
void stderr(String message) {
context.stderr.writeln(message);
}
@override
void stdout(String message) {
context.stdout.writeln(message);
}
@override
void trace(String message) {}
}

View file

@ -2,21 +2,11 @@
// 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 'dart:async';
import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
import 'package:analyzer_cli/src/fix/context.dart';
import 'package:cli_util/cli_logging.dart';
class TestContext with ResourceProviderMixin implements Context {
final StreamController<List<int>> stdinController =
new StreamController<List<int>>();
@override
final stdout = new StringBuffer();
@override
final stderr = new StringBuffer();
@override
String get workingDir => convertPath('/usr/some/non/existing/directory');
@ -30,10 +20,6 @@ class TestContext with ResourceProviderMixin implements Context {
@override
bool isDirectory(String filePath) => !filePath.endsWith('.dart');
void print([String text = '']) {
stdout.writeln(text);
}
}
class TestExit {
@ -41,3 +27,35 @@ class TestExit {
TestExit(this.code);
}
class TestLogger implements Logger {
final Ansi ansi;
final stdoutBuffer = new StringBuffer();
final stderrBuffer = new StringBuffer();
TestLogger() : this.ansi = new Ansi(false);
@override
void flush() {}
@override
bool get isVerbose => false;
@override
Progress progress(String message) {
return new SimpleProgress(this, message);
}
@override
void stderr(String message) {
stderrBuffer.writeln(message);
}
@override
void stdout(String message) {
stdoutBuffer.writeln(message);
}
@override
void trace(String message) {}
}