Add a new (hidden) library exposing a Command to run the formatter.
This Command has a new, nicer command-line interface.
diff --git a/bin/format.dart b/bin/format.dart
index eff4870..a11ded0 100644
--- a/bin/format.dart
+++ b/bin/format.dart
@@ -16,7 +16,7 @@
void main(List<String> args) {
var parser = ArgParser(allowTrailingOptions: true);
- defineOptions(parser);
+ defineOptions(parser, oldCli: true);
ArgResults argResults;
try {
diff --git a/lib/src/cli/format_command.dart b/lib/src/cli/format_command.dart
new file mode 100644
index 0000000..baea3e0
--- /dev/null
+++ b/lib/src/cli/format_command.dart
@@ -0,0 +1,156 @@
+// Copyright (c) 2020, 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 'dart:io';
+
+import 'package:args/command_runner.dart';
+
+import '../io.dart';
+import '../style_fix.dart';
+import 'formatter_options.dart';
+import 'options.dart';
+import 'output.dart';
+import 'show.dart';
+import 'summary.dart';
+
+class FormatCommand extends Command {
+ @override
+ String get name => 'format';
+
+ @override
+ String get description => 'Idiomatically formats Dart source code.';
+
+ @override
+ String get invocation =>
+ '${runner.executableName} $name [options...] <files or directories...>';
+
+ FormatCommand() {
+ defineOptions(argParser, oldCli: false);
+ }
+
+ @override
+ Future<int> run() async {
+ if (argResults['version']) {
+ print(dartStyleVersion);
+ return 0;
+ }
+
+ var show = const {
+ 'all': Show.all,
+ 'changed': Show.changed,
+ 'none': Show.none
+ }[argResults['show']];
+
+ var output = const {
+ 'write': Output.write,
+ 'show': Output.show,
+ 'none': Output.none,
+ 'json': Output.json,
+ }[argResults['output']];
+
+ var summary = Summary.none;
+ switch (argResults['summary'] as String) {
+ case 'line':
+ summary = Summary.line();
+ break;
+ case 'profile':
+ summary = Summary.profile();
+ break;
+ }
+
+ // If the user is sending code through stdin, default the output to stdout.
+ if (!argResults.wasParsed('output') && argResults.rest.isEmpty) {
+ output = Output.show;
+ }
+
+ // If the user wants to print the code and didn't indicate how the files
+ // should be printed, default to only showing the code.
+ if (!argResults.wasParsed('show') &&
+ (output == Output.show || output == Output.json)) {
+ show = Show.none;
+ }
+
+ // If the user wants JSON output, default to no summary.
+ if (!argResults.wasParsed('summary') && output == Output.json) {
+ summary = Summary.none;
+ }
+
+ // Can't use any summary with JSON output.
+ if (output == Output.json && summary != Summary.none) {
+ usageException('Cannot print a summary with JSON output.');
+ }
+
+ int pageWidth;
+ try {
+ pageWidth = int.parse(argResults['line-length']);
+ } on FormatException catch (_) {
+ usageException('--line-length must be an integer, was '
+ '"${argResults['line-length']}".');
+ }
+
+ int indent;
+ try {
+ indent = int.parse(argResults['indent']);
+ if (indent < 0 || indent.toInt() != indent) throw FormatException();
+ } on FormatException catch (_) {
+ usageException('--indent must be a non-negative integer, was '
+ '"${argResults['indent']}".');
+ }
+
+ var fixes = <StyleFix>[];
+ if (argResults['fix']) fixes.addAll(StyleFix.all);
+ for (var fix in StyleFix.all) {
+ if (argResults['fix-${fix.name}']) {
+ if (argResults['fix']) {
+ usageException('--fix-${fix.name} is redundant with --fix.');
+ }
+
+ fixes.add(fix);
+ }
+ }
+
+ List<int> selection;
+ try {
+ selection = parseSelection(argResults, 'selection');
+ } on FormatException catch (exception) {
+ usageException(exception.message);
+ }
+
+ var followLinks = argResults['follow-links'];
+ var setExitIfChanged = argResults['set-exit-if-changed'] as bool;
+
+ // If stdin isn't connected to a pipe, then the user is not passing
+ // anything to stdin, so let them know they made a mistake.
+ if (argResults.rest.isEmpty && stdin.hasTerminal) {
+ usageException('Missing paths to code to format.');
+ }
+
+ if (argResults.rest.isEmpty && output == Output.write) {
+ usageException('Cannot use --output=write when reading from stdin.');
+ }
+
+ if (argResults.wasParsed('stdin-name') && argResults.rest.isNotEmpty) {
+ usageException('Cannot pass --stdin-name when not reading from stdin.');
+ }
+ var stdinName = argResults['stdin-name'] as String;
+
+ var options = FormatterOptions(
+ indent: indent,
+ pageWidth: pageWidth,
+ followLinks: followLinks,
+ fixes: fixes,
+ show: show,
+ output: output,
+ summary: summary,
+ setExitIfChanged: setExitIfChanged);
+
+ if (argResults.rest.isEmpty) {
+ formatStdin(options, selection, stdinName);
+ } else {
+ formatPaths(options, argResults.rest);
+ options.summary.show();
+ }
+
+ return 0;
+ }
+}
diff --git a/lib/src/cli/options.dart b/lib/src/cli/options.dart
index 455a68b..2b94c76 100644
--- a/lib/src/cli/options.dart
+++ b/lib/src/cli/options.dart
@@ -5,22 +5,54 @@
import '../style_fix.dart';
-void defineOptions(ArgParser parser) {
+void defineOptions(ArgParser parser, {bool oldCli = false}) {
parser.addSeparator('Common options:');
- // Command implicitly adds "--help", so we only need to manually add it for
- // the old CLI.
- parser.addFlag('help',
- abbr: 'h', negatable: false, help: 'Shows this usage information.');
+ if (oldCli) {
+ // Command implicitly adds "--help", so we only need to manually add it for
+ // the old CLI.
+ parser.addFlag('help',
+ abbr: 'h', negatable: false, help: 'Shows this usage information.');
- parser.addFlag('overwrite',
- abbr: 'w',
- negatable: false,
- help: 'Overwrite input files with formatted output.');
- parser.addFlag('dry-run',
- abbr: 'n',
- negatable: false,
- help: 'Show which files would be modified but make no changes.');
+ parser.addFlag('overwrite',
+ abbr: 'w',
+ negatable: false,
+ help: 'Overwrite input files with formatted output.');
+ parser.addFlag('dry-run',
+ abbr: 'n',
+ negatable: false,
+ help: 'Show which files would be modified but make no changes.');
+ } else {
+ parser.addOption('output',
+ abbr: 'o',
+ help: 'Where formatted output should be written.',
+ allowed: ['write', 'show', 'json', 'none'],
+ allowedHelp: {
+ 'write': 'Overwrite formatted files on disc.',
+ 'show': 'Print code to terminal.',
+ 'json': 'Print code and selection as JSON',
+ 'none': 'Discard.'
+ },
+ defaultsTo: 'write');
+ parser.addOption('show',
+ help: 'Which filenames to print.',
+ allowed: ['all', 'changed', 'none'],
+ allowedHelp: {
+ 'all': 'All visited files and directories.',
+ 'changed': 'Only the names of files whose formatting is changed.',
+ 'none': 'No file names or directories.',
+ },
+ defaultsTo: 'changed');
+ parser.addOption('summary',
+ help: 'Summary shown after formatting completes.',
+ allowed: ['line', 'profile', 'none'],
+ allowedHelp: {
+ 'line': 'Single line summary.',
+ 'profile': 'Tracks how long it took for format each file.',
+ 'none': 'No summary.'
+ },
+ defaultsTo: 'line');
+ }
parser.addSeparator('Non-whitespace fixes (off by default):');
parser.addFlag('fix', negatable: false, help: 'Apply all style fixes.');
@@ -36,10 +68,12 @@
abbr: 'l', help: 'Wrap lines longer than this.', defaultsTo: '80');
parser.addOption('indent',
abbr: 'i', help: 'Spaces of leading indentation.', defaultsTo: '0');
- parser.addFlag('machine',
- abbr: 'm',
- negatable: false,
- help: 'Produce machine-readable JSON output.');
+ if (oldCli) {
+ parser.addFlag('machine',
+ abbr: 'm',
+ negatable: false,
+ help: 'Produce machine-readable JSON output.');
+ }
parser.addFlag('set-exit-if-changed',
negatable: false,
help: 'Return exit code 1 if there are any formatting changes.');
@@ -52,15 +86,18 @@
parser.addSeparator('Options when formatting from stdin:');
- parser.addOption('preserve',
+ parser.addOption(oldCli ? 'preserve' : 'selection',
help: 'Selection to preserve formatted as "start:length".');
parser.addOption('stdin-name',
help: 'The path name to show when an error occurs.',
defaultsTo: '<stdin>');
- parser.addFlag('profile', negatable: false, hide: true);
- // Ancient no longer used flag.
- parser.addFlag('transform', abbr: 't', negatable: false, hide: true);
+ if (oldCli) {
+ parser.addFlag('profile', negatable: false, hide: true);
+
+ // Ancient no longer used flag.
+ parser.addFlag('transform', abbr: 't', negatable: false, hide: true);
+ }
}
List<int> parseSelection(ArgResults argResults, String optionName) {
diff --git a/lib/src/cli/show.dart b/lib/src/cli/show.dart
index 30017ba..56734c9 100644
--- a/lib/src/cli/show.dart
+++ b/lib/src/cli/show.dart
@@ -5,6 +5,12 @@
/// Which file paths should be printed.
abstract class Show {
+ /// No files.
+ static const Show none = _NoneShow();
+
+ /// All traversed files.
+ static const Show all = _AllShow();
+
/// Only files whose formatting changed.
static const Show changed = _ChangedShow();
@@ -27,7 +33,7 @@
/// Describes a file that was processed.
///
/// Returns whether or not this file should be displayed.
- bool file(String path, {bool changed, bool overwritten}) => false;
+ bool file(String path, {bool changed, bool overwritten}) => true;
/// Describes the directory whose contents are about to be processed.
void directory(String path) {}
@@ -47,6 +53,25 @@
}
}
+class _NoneShow extends Show {
+ const _NoneShow() : super._();
+}
+
+class _AllShow extends Show {
+ const _AllShow() : super._();
+
+ @override
+ bool file(String path, {bool changed, bool overwritten}) {
+ if (changed) {
+ _showFileChange(path, overwritten: overwritten);
+ } else {
+ print('Unchanged $path');
+ }
+
+ return true;
+ }
+}
+
class _ChangedShow extends Show {
const _ChangedShow() : super._();
diff --git a/lib/src/cli/summary.dart b/lib/src/cli/summary.dart
index ab5013e..2d238ab 100644
--- a/lib/src/cli/summary.dart
+++ b/lib/src/cli/summary.dart
@@ -10,6 +10,10 @@
class Summary {
static const Summary none = Summary._();
+ /// Creates a Summary that tracks how many files were formatted and the total
+ /// time.
+ static Summary line() => _LineSummary();
+
/// Creates a Summary that captures profiling information.
///
/// Mostly for internal use.
@@ -31,6 +35,46 @@
void show() {}
}
+/// Tracks how many files were formatted and the total time.
+class _LineSummary extends Summary {
+ final DateTime _start = DateTime.now();
+
+ /// The number of processed files.
+ int _files = 0;
+
+ /// The number of changed files.
+ int _changed = 0;
+
+ _LineSummary() : super._();
+
+ /// Describe the processed file at [path] whose formatted result is [output].
+ ///
+ /// If the contents of the file are the same as the formatted output,
+ /// [changed] will be false.
+ @override
+ void afterFile(FormatterOptions options, File file, String displayPath,
+ SourceCode output,
+ {bool changed}) {
+ _files++;
+ if (changed) _changed++;
+ }
+
+ /// Show the times for the slowest files to format.
+ @override
+ void show() {
+ var elapsed = DateTime.now().difference(_start);
+ var time = (elapsed.inMilliseconds / 1000).toStringAsFixed(2);
+
+ if (_files == 0) {
+ print('Formatted no files in $time seconds.');
+ } else if (_files == 1) {
+ print('Formatted $_files file ($_changed changed) in $time seconds.');
+ } else {
+ print('Formatted $_files files ($_changed changed) in $time seconds.');
+ }
+ }
+}
+
/// Reports how long it took for format each file.
class _ProfileSummary implements Summary {
/// The files that have been started but have not completed yet.
diff --git a/test/command_test.dart b/test/command_test.dart
new file mode 100644
index 0000000..32ed262
--- /dev/null
+++ b/test/command_test.dart
@@ -0,0 +1,456 @@
+// Copyright (c) 2014, 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 'dart:convert';
+
+import 'package:path/path.dart' as p;
+import 'package:test_descriptor/test_descriptor.dart' as d;
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ test('formats a directory', () async {
+ await d.dir('code', [
+ d.file('a.dart', unformattedSource),
+ d.file('b.dart', formattedSource),
+ d.file('c.dart', unformattedSource)
+ ]).create();
+
+ var process = await runCommandOnDir();
+ expect(await process.stdout.next, 'Formatted code/a.dart');
+ expect(await process.stdout.next, 'Formatted code/c.dart');
+ expect(await process.stdout.next,
+ startsWith(r'Formatted 3 files (2 changed)'));
+ await process.shouldExit(0);
+
+ // Overwrites the files.
+ await d.dir('code', [d.file('a.dart', formattedSource)]).validate();
+ await d.dir('code', [d.file('c.dart', formattedSource)]).validate();
+ });
+
+ test('formats multiple paths', () async {
+ await d.dir('code', [
+ d.dir('subdir', [
+ d.file('a.dart', unformattedSource),
+ ]),
+ d.file('b.dart', unformattedSource),
+ d.file('c.dart', unformattedSource)
+ ]).create();
+
+ var process = await runCommand(['code/subdir', 'code/c.dart']);
+ expect(await process.stdout.next, 'Formatted code/subdir/a.dart');
+ expect(await process.stdout.next, 'Formatted code/c.dart');
+ expect(await process.stdout.next,
+ startsWith(r'Formatted 2 files (2 changed)'));
+ await process.shouldExit(0);
+
+ // Overwrites the selected files.
+ await d.dir('code', [
+ d.dir('subdir', [
+ d.file('a.dart', formattedSource),
+ ]),
+ d.file('b.dart', unformattedSource),
+ d.file('c.dart', formattedSource)
+ ]).validate();
+ });
+
+ test('exits with 64 on a command line argument error', () async {
+ var process = await runCommand(['-wat']);
+ await process.shouldExit(64);
+ });
+
+ test('exits with 65 on a parse error', () async {
+ await d.dir('code', [d.file('a.dart', 'herp derp i are a dart')]).create();
+
+ var process = await runCommandOnDir();
+ await process.shouldExit(65);
+ });
+
+ group('--show', () {
+ test('all shows all files', () async {
+ await d.dir('code', [
+ d.file('a.dart', unformattedSource),
+ d.file('b.dart', formattedSource),
+ d.file('c.dart', unformattedSource)
+ ]).create();
+
+ var process = await runCommandOnDir(['--show=all']);
+ expect(await process.stdout.next, 'Formatted code/a.dart');
+ expect(await process.stdout.next, 'Unchanged code/b.dart');
+ expect(await process.stdout.next, 'Formatted code/c.dart');
+ expect(await process.stdout.next,
+ startsWith(r'Formatted 3 files (2 changed)'));
+ await process.shouldExit(0);
+ });
+
+ test('none shows nothing', () async {
+ await d.dir('code', [
+ d.file('a.dart', unformattedSource),
+ d.file('b.dart', formattedSource),
+ d.file('c.dart', unformattedSource)
+ ]).create();
+
+ var process = await runCommandOnDir(['--show=none']);
+ expect(await process.stdout.next,
+ startsWith(r'Formatted 3 files (2 changed)'));
+ await process.shouldExit(0);
+ });
+
+ test('changed shows changed files', () async {
+ await d.dir('code', [
+ d.file('a.dart', unformattedSource),
+ d.file('b.dart', formattedSource),
+ d.file('c.dart', unformattedSource)
+ ]).create();
+
+ var process = await runCommandOnDir(['--show=changed']);
+ expect(await process.stdout.next, 'Formatted code/a.dart');
+ expect(await process.stdout.next, 'Formatted code/c.dart');
+ expect(await process.stdout.next,
+ startsWith(r'Formatted 3 files (2 changed)'));
+ await process.shouldExit(0);
+ });
+ });
+
+ group('--output', () {
+ group('show', () {
+ test('prints only formatted output by default', () async {
+ await d.dir('code', [
+ d.file('a.dart', unformattedSource),
+ d.file('b.dart', formattedSource)
+ ]).create();
+
+ var process = await runCommandOnDir(['--output=show']);
+ expect(await process.stdout.next, formattedOutput);
+ expect(await process.stdout.next, formattedOutput);
+ expect(await process.stdout.next,
+ startsWith(r'Formatted 2 files (1 changed)'));
+ await process.shouldExit(0);
+
+ // Does not overwrite files.
+ await d.dir('code', [d.file('a.dart', unformattedSource)]).validate();
+ });
+
+ test('with --show=all prints all files and names first', () async {
+ await d.dir('code', [
+ d.file('a.dart', unformattedSource),
+ d.file('b.dart', formattedSource)
+ ]).create();
+
+ var process = await runCommandOnDir(['--output=show', '--show=all']);
+ expect(await process.stdout.next, 'Changed code/a.dart');
+ expect(await process.stdout.next, formattedOutput);
+ expect(await process.stdout.next, 'Unchanged code/b.dart');
+ expect(await process.stdout.next, formattedOutput);
+ expect(await process.stdout.next,
+ startsWith(r'Formatted 2 files (1 changed)'));
+ await process.shouldExit(0);
+
+ // Does not overwrite files.
+ await d.dir('code', [d.file('a.dart', unformattedSource)]).validate();
+ });
+
+ test('with --show=changed prints only changed files', () async {
+ await d.dir('code', [
+ d.file('a.dart', unformattedSource),
+ d.file('b.dart', formattedSource)
+ ]).create();
+
+ var process =
+ await runCommandOnDir(['--output=show', '--show=changed']);
+ expect(await process.stdout.next, 'Changed code/a.dart');
+ expect(await process.stdout.next, formattedOutput);
+ expect(await process.stdout.next,
+ startsWith(r'Formatted 2 files (1 changed)'));
+ await process.shouldExit(0);
+
+ // Does not overwrite files.
+ await d.dir('code', [d.file('a.dart', unformattedSource)]).validate();
+ });
+ });
+
+ group('json', () {
+ test('writes each output as json', () async {
+ await d.dir('code', [
+ d.file('a.dart', unformattedSource),
+ d.file('b.dart', unformattedSource)
+ ]).create();
+
+ var jsonA = jsonEncode({
+ 'path': p.join('code', 'a.dart'),
+ 'source': formattedSource,
+ 'selection': {'offset': -1, 'length': -1}
+ });
+
+ var jsonB = jsonEncode({
+ 'path': p.join('code', 'b.dart'),
+ 'source': formattedSource,
+ 'selection': {'offset': -1, 'length': -1}
+ });
+
+ var process = await runCommandOnDir(['--output=json']);
+
+ expect(await process.stdout.next, jsonA);
+ expect(await process.stdout.next, jsonB);
+ await process.shouldExit();
+ });
+
+ test('errors if the summary is not none', () async {
+ var process =
+ await runCommandOnDir(['--output=json', '--summary=line']);
+ await process.shouldExit(64);
+ });
+ });
+
+ group('none', () {
+ test('with --show=all prints only names', () async {
+ await d.dir('code', [
+ d.file('a.dart', unformattedSource),
+ d.file('b.dart', formattedSource)
+ ]).create();
+
+ var process = await runCommandOnDir(['--output=none', '--show=all']);
+ expect(await process.stdout.next, 'Changed code/a.dart');
+ expect(await process.stdout.next, 'Unchanged code/b.dart');
+ expect(await process.stdout.next,
+ startsWith(r'Formatted 2 files (1 changed)'));
+ await process.shouldExit(0);
+
+ // Does not overwrite files.
+ await d.dir('code', [d.file('a.dart', unformattedSource)]).validate();
+ });
+
+ test('with --show=changed prints only changed names', () async {
+ await d.dir('code', [
+ d.file('a.dart', unformattedSource),
+ d.file('b.dart', formattedSource)
+ ]).create();
+
+ var process =
+ await runCommandOnDir(['--output=none', '--show=changed']);
+ expect(await process.stdout.next, 'Changed code/a.dart');
+ expect(await process.stdout.next,
+ startsWith(r'Formatted 2 files (1 changed)'));
+ await process.shouldExit(0);
+
+ // Does not overwrite files.
+ await d.dir('code', [d.file('a.dart', unformattedSource)]).validate();
+ });
+ });
+ });
+
+ group('--summary', () {
+ test('line', () async {
+ await d.dir('code', [
+ d.file('a.dart', unformattedSource),
+ d.file('b.dart', formattedSource)
+ ]).create();
+
+ var process = await runCommandOnDir(['--summary=line']);
+ expect(await process.stdout.next, 'Formatted code/a.dart');
+ expect(await process.stdout.next,
+ matches(r'Formatted 2 files \(1 changed\) in \d+\.\d+ seconds.'));
+ await process.shouldExit(0);
+ });
+ });
+
+ test('--version prints the version number', () async {
+ var process = await runCommand(['--version']);
+
+ // Match something roughly semver-like.
+ expect(await process.stdout.next, matches(RegExp(r'\d+\.\d+\.\d+.*')));
+ await process.shouldExit(0);
+ });
+
+ test('--help', () async {
+ var process = await runCommand(['--help']);
+ expect(
+ await process.stdout.next, 'Idiomatically formats Dart source code.');
+ await process.shouldExit(0);
+ });
+
+ group('fix', () {
+ test('--fix applies all fixes', () async {
+ var process = await runCommand(['--fix', '--output=show']);
+ process.stdin.writeln('foo({a:1}) {');
+ process.stdin.writeln(' new Bar(const Baz(const []));}');
+ await process.stdin.close();
+
+ expect(await process.stdout.next, 'foo({a = 1}) {');
+ expect(await process.stdout.next, ' Bar(const Baz([]));');
+ expect(await process.stdout.next, '}');
+ await process.shouldExit(0);
+ });
+
+ test('--fix-named-default-separator', () async {
+ var process =
+ await runCommand(['--fix-named-default-separator', '--output=show']);
+ process.stdin.writeln('foo({a:1}) {');
+ process.stdin.writeln(' new Bar();}');
+ await process.stdin.close();
+
+ expect(await process.stdout.next, 'foo({a = 1}) {');
+ expect(await process.stdout.next, ' new Bar();');
+ expect(await process.stdout.next, '}');
+ await process.shouldExit(0);
+ });
+
+ test('--fix-optional-const', () async {
+ var process = await runCommand(['--fix-optional-const', '--output=show']);
+ process.stdin.writeln('foo({a:1}) {');
+ process.stdin.writeln(' const Bar(const Baz());}');
+ await process.stdin.close();
+
+ expect(await process.stdout.next, 'foo({a: 1}) {');
+ expect(await process.stdout.next, ' const Bar(Baz());');
+ expect(await process.stdout.next, '}');
+ await process.shouldExit(0);
+ });
+
+ test('--fix-optional-new', () async {
+ var process = await runCommand(['--fix-optional-new', '--output=show']);
+ process.stdin.writeln('foo({a:1}) {');
+ process.stdin.writeln(' new Bar();}');
+ await process.stdin.close();
+
+ expect(await process.stdout.next, 'foo({a: 1}) {');
+ expect(await process.stdout.next, ' Bar();');
+ expect(await process.stdout.next, '}');
+ await process.shouldExit(0);
+ });
+
+ test('errors with --fix and specific fix flag', () async {
+ var process =
+ await runCommand(['--fix', '--fix-named-default-separator']);
+ await process.shouldExit(64);
+ });
+ });
+
+ group('--indent', () {
+ test('sets the leading indentation of the output', () async {
+ var process = await runCommand(['--indent=3']);
+ process.stdin.writeln("main() {'''");
+ process.stdin.writeln("a flush left multi-line string''';}");
+ await process.stdin.close();
+
+ expect(await process.stdout.next, ' main() {');
+ expect(await process.stdout.next, " '''");
+ expect(await process.stdout.next, "a flush left multi-line string''';");
+ expect(await process.stdout.next, ' }');
+ await process.shouldExit(0);
+ });
+
+ test('errors if the indent is not a non-negative number', () async {
+ var process = await runCommand(['--indent=notanum']);
+ await process.shouldExit(64);
+
+ process = await runCommand(['--indent=-4']);
+ await process.shouldExit(64);
+ });
+ });
+
+ group('--set-exit-if-changed', () {
+ test('gives exit code 0 if there are no changes', () async {
+ await d.dir('code', [d.file('a.dart', formattedSource)]).create();
+
+ var process = await runCommandOnDir(['--set-exit-if-changed']);
+ await process.shouldExit(0);
+ });
+
+ test('gives exit code 1 if there are changes', () async {
+ await d.dir('code', [d.file('a.dart', unformattedSource)]).create();
+
+ var process = await runCommandOnDir(['--set-exit-if-changed']);
+ await process.shouldExit(1);
+ });
+
+ test('gives exit code 1 if there are changes when not writing', () async {
+ await d.dir('code', [d.file('a.dart', unformattedSource)]).create();
+
+ var process =
+ await runCommandOnDir(['--set-exit-if-changed', '--show=none']);
+ await process.shouldExit(1);
+ });
+ });
+
+ group('--selection', () {
+ test('errors if given path', () async {
+ var process = await runCommand(['--selection', 'path']);
+ await process.shouldExit(64);
+ });
+
+ test('errors on wrong number of components', () async {
+ var process = await runCommand(['--selection', '1']);
+ await process.shouldExit(64);
+
+ process = await runCommand(['--selection', '1:2:3']);
+ await process.shouldExit(64);
+ });
+
+ test('errors on non-integer component', () async {
+ var process = await runCommand(['--selection', '1:2.3']);
+ await process.shouldExit(64);
+ });
+
+ test('updates selection', () async {
+ var process = await runCommand(['--output=json', '--selection=6:10']);
+ process.stdin.writeln(unformattedSource);
+ await process.stdin.close();
+
+ var json = jsonEncode({
+ 'path': '<stdin>',
+ 'source': formattedSource,
+ 'selection': {'offset': 5, 'length': 9}
+ });
+
+ expect(await process.stdout.next, json);
+ await process.shouldExit();
+ });
+ });
+
+ group('--stdin-name', () {
+ test('errors if given path', () async {
+ var process = await runCommand(['--stdin-name=name', 'path']);
+ await process.shouldExit(64);
+ });
+ });
+
+ group('with no paths', () {
+ test('errors on --output=write', () async {
+ var process = await runCommand(['--output=write']);
+ await process.shouldExit(64);
+ });
+
+ test('exits with 65 on parse error', () async {
+ var process = await runCommand();
+ process.stdin.writeln('herp derp i are a dart');
+ await process.stdin.close();
+ await process.shouldExit(65);
+ });
+
+ test('reads from stdin', () async {
+ var process = await runCommand();
+ process.stdin.writeln(unformattedSource);
+ await process.stdin.close();
+
+ // No trailing newline at the end.
+ expect(await process.stdout.next, formattedOutput);
+ await process.shouldExit(0);
+ });
+
+ test('allows specifying stdin path name', () async {
+ var process = await runCommand(['--stdin-name=some/path.dart']);
+ process.stdin.writeln('herp');
+ await process.stdin.close();
+
+ expect(await process.stderr.next,
+ 'Could not format because the source could not be parsed:');
+ expect(await process.stderr.next, '');
+ expect(await process.stderr.next, contains('some/path.dart'));
+ await process.stderr.cancel();
+ await process.shouldExit(65);
+ });
+ });
+}
diff --git a/test/utils.dart b/test/utils.dart
index 4f99ccf..da3d766 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -27,8 +27,6 @@
/// Runs the command line formatter, passing it [args].
Future<TestProcess> runFormatter([List<String> args]) {
- args ??= [];
-
// Locate the "test" directory. Use mirrors so that this works with the test
// package, which loads this suite into an isolate.
var testDir = p.dirname(currentMirrorSystem()
@@ -37,22 +35,36 @@
.toFilePath());
var formatterPath = p.normalize(p.join(testDir, '../bin/format.dart'));
-
- args.insert(0, formatterPath);
-
- // Use the same package root, if there is one.
- if (Platform.packageConfig != null && Platform.packageConfig.isNotEmpty) {
- args.insert(0, '--packages=${Platform.packageConfig}');
- }
-
- return TestProcess.start(Platform.executable, args);
+ return TestProcess.start(Platform.executable, [formatterPath, ...?args],
+ workingDirectory: d.sandbox);
}
/// Runs the command line formatter, passing it the test directory followed by
/// [args].
Future<TestProcess> runFormatterOnDir([List<String> args]) {
- args ??= [];
- return runFormatter([d.sandbox, ...args]);
+ return runFormatter(['.', ...?args]);
+}
+
+/// Runs the test shell for the [Command]-based formatter, passing it [args].
+Future<TestProcess> runCommand([List<String> args]) {
+ // Locate the "test" directory. Use mirrors so that this works with the test
+ // package, which loads this suite into an isolate.
+ var testDir = p.dirname(currentMirrorSystem()
+ .findLibrary(#dart_style.test.utils)
+ .uri
+ .toFilePath());
+
+ var formatterPath =
+ p.normalize(p.join(testDir, '../tool/command_shell.dart'));
+ return TestProcess.start(
+ Platform.executable, [formatterPath, 'format', ...?args],
+ workingDirectory: d.sandbox);
+}
+
+/// Runs the test shell for the [Command]-based formatter, passing it the test
+/// directory followed by [args].
+Future<TestProcess> runCommandOnDir([List<String> args]) {
+ return runCommand(['.', ...?args]);
}
/// Run tests defined in "*.unit" and "*.stmt" files inside directory [name].
diff --git a/tool/command_shell.dart b/tool/command_shell.dart
new file mode 100644
index 0000000..e9f3531
--- /dev/null
+++ b/tool/command_shell.dart
@@ -0,0 +1,25 @@
+// Copyright (c) 2020, 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 'dart:io';
+
+import 'package:args/command_runner.dart';
+
+import 'package:dart_style/src/cli/format_command.dart';
+
+/// A simple executable wrapper around the [Command] API defined by dart_style.
+///
+/// This enables tests to spawn this executable in order to verify the output
+/// it prints.
+void main(List<String> command) async {
+ var runner =
+ CommandRunner('dartfmt', 'Idiomatically formats Dart source code.');
+ runner.addCommand(FormatCommand());
+
+ try {
+ await runner.runCommand(runner.parse(command));
+ } on UsageException catch (exception) {
+ stderr.writeln(exception);
+ exit(64);
+ }
+}