Permalink
Browse files

Add support for non-whitespace opt-in "style fixes". (#694)

Implement the first fix, converting ":" to "=" as the named default
value separator.
  • Loading branch information...
munificent committed Jun 8, 2018
1 parent 674d6eb commit 9eb5c5d38a197c6c3b65fb4172490b81b4f1e1d8
@@ -1,3 +1,8 @@
# 1.1.0-dev
* Add support for "style fixes", opt-in non-whitespace changes.
* Add fix to convert ":" to "=" as the named parameter default value separator.
# 1.0.14
* Support metadata on enum cases (#688).
@@ -32,6 +32,29 @@ if (tag == 'style' ||
The formatter will never break your code—you can safely invoke it
automatically from build and presubmit scripts.
## Style fixes
The formatter can also apply non-whitespace changes to make your code
consistently idiomatic. You must opt into these by passing either `--fix` which
applies all style fixes, or any of the `--fix-`-prefixed flags to apply
specific fixes.
For example, running with `--fix-named-default-separator` changes this:
```dart
greet(String name, {String title: "Captain"}) {
print("Greetings, $title $name!");
}
```
into:
```dart
greet(String name, {String title = "Captain"}) {
print("Greetings, $title $name!");
}
```
## Getting dartfmt
Dartfmt is included in the Dart SDK, so you might want to add the SDK's bin
@@ -12,48 +12,57 @@ import 'package:dart_style/src/exceptions.dart';
import 'package:dart_style/src/formatter_options.dart';
import 'package:dart_style/src/io.dart';
import 'package:dart_style/src/source_code.dart';
import 'package:dart_style/src/style_fix.dart';
// Note: The following line of code is modified by tool/grind.dart.
const version = "1.0.14";
void main(List<String> args) {
var parser = new ArgParser(allowTrailingOptions: true);
parser.addSeparator("Common options:");
parser.addFlag("help",
abbr: "h", negatable: false, help: "Shows usage information.");
parser.addFlag("version",
negatable: false, help: "Shows version information.");
parser.addOption("line-length",
abbr: "l", help: "Wrap lines longer than this.", defaultsTo: "80");
parser.addOption("indent",
abbr: "i", help: "Spaces of leading indentation.", defaultsTo: "0");
parser.addOption("preserve",
help: 'Selection to preserve, formatted as "start:length".');
parser.addFlag("dry-run",
abbr: "n",
negatable: false,
help: "Show which files would be modified but make no changes.");
parser.addFlag("set-exit-if-changed",
negatable: false,
help: "Return exit code 1 if there are any formatting 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.");
parser.addSeparator("Non-whitespace fixes (off by default):");
parser.addFlag("fix", negatable: false, help: "Apply all style fixes.");
for (var fix in StyleFix.all) {
// TODO(rnystrom): Allow negating this if used in concert with "--fix"?
parser.addFlag("fix-${fix.name}", negatable: false, help: fix.description);
}
parser.addSeparator("Other options:");
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.");
parser.addFlag("profile",
negatable: false, help: "Display profile times after running.");
parser.addFlag("set-exit-if-changed",
negatable: false,
help: "Return exit code 1 if there are any formatting changes.");
parser.addFlag("follow-links",
negatable: false,
help: "Follow links to files and directories.\n"
"If unset, links will be ignored.");
parser.addFlag("transform",
abbr: "t",
negatable: false,
help: "Unused flag for compability with the old formatter.");
parser.addOption("preserve",
help: 'Selection to preserve, formatted as "start:length".');
parser.addFlag("profile", negatable: false, hide: true);
parser.addFlag("transform", abbr: "t", negatable: false, hide: true);
ArgResults argResults;
try {
@@ -74,7 +83,6 @@ void main(List<String> args) {
// Can only preserve a selection when parsing from stdin.
List<int> selection;
if (argResults["preserve"] != null && argResults.rest.isNotEmpty) {
usageError(parser, "Can only use --preserve when reading from stdin.");
}
@@ -150,8 +158,23 @@ void main(List<String> args) {
var followLinks = argResults["follow-links"];
var fixes = <StyleFix>[];
if (argResults["fix"]) fixes.addAll(StyleFix.all);
for (var fix in StyleFix.all) {
if (argResults["fix-${fix.name}"]) {
if (argResults["fix"]) {
usageError(parser, "--fix-${fix.name} is redundant with --fix.");
}
fixes.add(fix);
}
}
var options = new FormatterOptions(reporter,
indent: indent, pageWidth: pageWidth, followLinks: followLinks);
indent: indent,
pageWidth: pageWidth,
followLinks: followLinks,
fixes: fixes);
if (argResults.rest.isEmpty) {
formatStdin(options, selection);
@@ -188,8 +211,10 @@ void formatStdin(FormatterOptions options, List<int> selection) {
var input = new StringBuffer();
stdin.transform(new Utf8Decoder()).listen(input.write, onDone: () {
var formatter =
new DartFormatter(indent: options.indent, pageWidth: options.pageWidth);
var formatter = new DartFormatter(
indent: options.indent,
pageWidth: options.pageWidth,
fixes: options.fixes);
try {
options.reporter.beforeFile(null, "<stdin>");
var source = new SourceCode(input.toString(),
@@ -244,15 +269,18 @@ void usageError(ArgParser parser, String error) {
void printUsage(ArgParser parser, [String error]) {
var output = stdout;
var message = "Reformats whitespace in Dart source files.";
var message = "Idiomatically formats Dart source code.";
if (error != null) {
message = error;
output = stdout;
}
output.write("""$message
Usage: dartfmt [-n|-w] [files or directories...]
Usage: dartfmt [options...] [files or directories...]
Example: dartfmt -w .
Reformats every Dart file in the current directory tree.
${parser.usage}
""");
@@ -6,4 +6,5 @@ library dart_style;
export 'src/dart_formatter.dart';
export 'src/exceptions.dart';
export 'src/style_fix.dart';
export 'src/source_code.dart';
@@ -19,6 +19,7 @@ import 'exceptions.dart';
import 'source_code.dart';
import 'source_visitor.dart';
import 'string_compare.dart' as string_compare;
import 'style_fix.dart';
/// Dart source code formatter.
class DartFormatter {
@@ -35,6 +36,8 @@ class DartFormatter {
/// The number of characters of indentation to prefix the output lines with.
final int indent;
final Set<StyleFix> fixes = new Set();
/// Creates a new formatter for Dart code.
///
/// If [lineEnding] is given, that will be used for any newlines in the
@@ -43,8 +46,14 @@ class DartFormatter {
///
/// If [indent] is given, that many levels of indentation will be prefixed
/// before each resulting line in the output.
DartFormatter({this.lineEnding, int pageWidth, this.indent: 0})
: this.pageWidth = (pageWidth == null) ? 80 : pageWidth;
///
/// While formatting, also applies any of the given [fixes].
DartFormatter(
{this.lineEnding, int pageWidth, int indent, Iterable<StyleFix> fixes})
: pageWidth = pageWidth ?? 80,
indent = indent ?? 0 {
if (fixes != null) this.fixes.addAll(fixes);
}
/// Formats the given [source] string containing an entire Dart compilation
/// unit.
@@ -130,7 +139,10 @@ class DartFormatter {
// Format it.
var visitor = new SourceVisitor(this, lineInfo, source);
var output = visitor.run(node);
if (!string_compare.equalIgnoringWhitespace(source.text, output.text)) {
// Sanity check that only whitespace was changed if that's all we expect.
if (fixes.isEmpty &&
!string_compare.equalIgnoringWhitespace(source.text, output.text)) {
throw new UnexpectedOutputException(source.text, output.text);
}
@@ -8,6 +8,7 @@ import 'dart:convert';
import 'dart:io';
import 'source_code.dart';
import 'style_fix.dart';
/// Global options that affect how the formatter produces and uses its outputs.
class FormatterOptions {
@@ -24,8 +25,14 @@ class FormatterOptions {
/// Whether symlinks should be traversed when formatting a directory.
final bool followLinks;
/// The style fixes to apply while formatting.
final Iterable<StyleFix> fixes;
FormatterOptions(this.reporter,
{this.indent: 0, this.pageWidth: 80, this.followLinks: false});
{this.indent: 0,
this.pageWidth: 80,
this.followLinks: false,
this.fixes});
}
/// How the formatter reports the results it produces.
@@ -67,8 +67,10 @@ bool processDirectory(FormatterOptions options, Directory directory) {
bool processFile(FormatterOptions options, File file, {String label}) {
if (label == null) label = file.path;
var formatter =
new DartFormatter(indent: options.indent, pageWidth: options.pageWidth);
var formatter = new DartFormatter(
indent: options.indent,
pageWidth: options.pageWidth,
fixes: options.fixes);
try {
var source = new SourceCode(file.readAsStringSync(), uri: file.path);
options.reporter.beforeFile(file, label);
@@ -19,6 +19,7 @@ import 'rule/metadata.dart';
import 'rule/rule.dart';
import 'rule/type_argument.dart';
import 'source_code.dart';
import 'style_fix.dart';
import 'whitespace.dart';
/// Visits every token of the AST and passes all of the relevant bits to a
@@ -831,9 +832,16 @@ class SourceVisitor extends ThrowingAstVisitor {
builder.startSpan();
builder.nestExpression();
// The '=' separator is preceded by a space, ":" is not.
if (node.separator.type == TokenType.EQ) space();
token(node.separator);
if (_formatter.fixes.contains(StyleFix.namedDefaultSeparator)) {
// Change the separator to "=".
space();
writePrecedingCommentsAndNewlines(node.separator);
_writeText("=", node.separator.offset);
} else {
// The '=' separator is preceded by a space, ":" is not.
if (node.separator.type == TokenType.EQ) space();
token(node.separator);
}
soloSplit(_assignmentCost(node.defaultValue));
visit(node.defaultValue);
@@ -0,0 +1,18 @@
// 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.
/// Enum-like class for the different syntactic fixes that can be applied while
/// formatting.
class StyleFix {
static const namedDefaultSeparator = const StyleFix._(
"named-default-separator",
'Use "=" as the separator before named parameter default values.');
static const all = const [namedDefaultSeparator];
final String name;
final String description;
const StyleFix._(this.name, this.description);
}
@@ -1,6 +1,6 @@
name: dart_style
# Note: See tool/grind.dart for how to bump the version.
version: 1.0.14
version: 1.1.0-dev
author: Dart Team <misc@dartlang.org>
description: Opinionated, automatic Dart source code formatter.
homepage: https://github.com/dart-lang/dart_style
@@ -233,6 +233,33 @@ void main() {
});
});
group("fix", () {
// TODO(rnystrom): This will get more useful when other fixes are supported.
test("--fix applies all fixes", () async {
var process = await runFormatter(["--fix"]);
process.stdin.writeln("foo({a:1}) {}");
await process.stdin.close();
expect(await process.stdout.next, "foo({a = 1}) {}");
await process.shouldExit(0);
});
test("--fix-named-default-separator", () async {
var process = await runFormatter(["--fix-named-default-separator"]);
process.stdin.writeln("foo({a:1}) {}");
await process.stdin.close();
expect(await process.stdout.next, "foo({a = 1}) {}");
await process.shouldExit(0);
});
test("errors with --fix and specific fix flag", () async {
var process =
await runFormatter(["--fix", "--fix-named-default-separator"]);
await process.shouldExit(64);
});
});
group("with no paths", () {
test("errors on --overwrite", () async {
var process = await runFormatter(["--overwrite"]);
@@ -0,0 +1,17 @@
// 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.
@TestOn("vm")
library dart_style.test.fix_test;
import 'package:test/test.dart';
import 'package:dart_style/dart_style.dart';
import 'utils.dart';
void main() {
testFile(
"fixes/named_default_separator.unit", [StyleFix.namedDefaultSeparator]);
}
Oops, something went wrong.

0 comments on commit 9eb5c5d

Please sign in to comment.