-
Notifications
You must be signed in to change notification settings - Fork 125
FAQ
The formatter has a few goals, in order of descending priority:
-
Produce consistently formatted code. Consistent style improves readability because you aren't distracted by differences in style between different parts of a program. Consistency makes it easier to contribute to others' code because their style will already be familiar to you.
-
End debates about style issues in code reviews. This consumes an astonishingly large quantity of valuable engineering energy. Style debates are time-consuming, upset people, and rarely change anyone's mind. They make code reviews take longer and leave participants feeling bad.
-
Free users from having to think about and apply formatting. When writing code, you don't have to try to figure out the best way to split a line and then painstakingly add in the line breaks. When you do a global refactor that changes the length of some identifier, you don't have to go back and rewrap all of the lines. When you're in the zone, you can just pump out code and let the formatter tidy it up for you as you go.
-
Produce beautiful, readable output that helps users understand the code. We could solve all of the above goals with a formatter that just removed all whitespace, but that wouldn't be very human-friendly. So, finally, the formatter tries very hard to produce output that is not just consistent but readable to a human. It tries to use indentation and line breaks to highlight the structure and organization of the code.
In several cases, the formatter has pointed out bugs where the existing indentation was misleading and didn't represent what the code actually did. For example, automated formatting would have helped make Apple's "gotofail" security bug easier to notice:
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0) goto fail; goto fail; if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0) goto fail;
The formatter would change this to:
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0) goto fail; goto fail; // <-- Now clearly not under the "if". if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0) goto fail;
First of all, that's not a question. But, yes, sometimes you may dislike the output of the formatter. This may be a bug or it may be a deliberate stylistic choice of the formatter that you disagree with. The simplest way to find out is to file an issue.
Now that the formatter is pretty mature, it's more likely that the output is deliberate. If you still aren't happy with what it did to your code, the easiest thing you can do is tweak what you send it. While the formatter tries to make all code look great, there are trade-offs in some of the rules. In those cases, it leans towards making more common idioms look better.
If your code ends up looking bad, your code may be off the beaten path. Usually hoisting an expression up to a local variable or taking a big function expression out and making it a named function is all that's needed to get back to a happy place.
See here: Configuration.
You can rely on the formatter to not break your code or change its semantics. If it does, this is a critical bug and we'll fix it quickly. The formatter also has internal sanity checks to validate that its output doesn't corrupt the code. The formatter is used every day inside and outside of Google on millions of lines of Dart code. Most users have their IDE set to format every time they save a file, so the formatter is likely executed millions of times a day.
This means both that it is very heavily vetted and also that it doesn't change very often. The rules the formatter uses to determine the "best" way to split a line may change over time, mostly in complex cases. We don't promise that code produced by the formatter today will be identical to the same code run through a later version of the formatter. We do hope that you'll like the output of the later version more.
Since we are in the middle of transitioning to a new style, there will likely be more style rule changes for a while as we get feedback from users. But the new style has been heavily tested internally and externally, so we expect it to settle down fairly quickly.
I wrote a long article about how the formatter is implemented here. The new "tall" style implementation is architecturally different in some ways but that article should still give you the general flavor of how it works.
The formatter has two basic styles for formatting a function call:
// Fit everything on one line:
function(argument1, argument2, argument3);
// Split around every argument:
function(
argument1,
argument2,
argument3,
);
Those work fine in most cases. But you probably wouldn't want all of your tests to look like:
test(
"adds two numbers correctly",
() {
expect(1 + 2, equals(3));
},
);
Instead, you probably expect something like:
test("adds two numbers correctly", () {
expect(1 + 2, equals(3));
});
This "block-formatted" argument list style is so natural in many places that you may not even notice it:
argParser.addAll([
"--help",
"--mode",
"debug"
]);
Deciding which arguments should get this block formatting is one of the most subtle corners of the formatter's heuristics. Sometimes you run into code that seems like it really should use one style but the formatter picks the other. Often, you can get it back onto a happy path by reorganizing your code a bit. Perhaps hoist one of the arguments out to a separate local variable or break a long string literal in half.
If that doesn't help, file an issue with the code in question and it may be possible to tweak the rules. Otherwise, accept that the formatter is doing the best it can with its very limited understanding of your code.
Large collection literals are often used to define big chunks of structured data, like:
/// Maps ASCII character values to what kind of character they represent.
const characterTypes = const [
other, other, other, other, other, other, other, other,
other, white, white, other, other, white,
other, other, other, other, other, other, other, other,
other, other, other, other, other, other, other, other,
other, other, white,
punct, other, punct, punct, punct, punct, other,
brace, brace, punct, punct, comma, punct, punct, punct,
digit, digit, digit, digit, digit,
digit, digit, digit, digit, digit,
punct, punct, punct, punct, punct, punct, punct,
alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha,
alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha,
alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha,
alpha, alpha, brace, punct, brace, punct, alpha, other,
alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha,
alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha,
alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha,
alpha, alpha, brace, punct, brace, punct
];
The formatter doesn't know those newlines are meaningful, so it wipes it out to:
/// Maps ASCII character values to what kind of character they represent.
const characterTypes = const [
other,
other,
other,
// lots more ...
punct,
brace,
punct
];
In many cases, ignoring these newlines is a good thing. If you've removed a few items from a list, it's a win for the formatter to repack it into one line if it fits. But here it clearly loses useful information.
Fortunately, in most cases, structured collections like this have comments describing their structure:
const characterTypes = const [
other, other, other, other, other, other, other, other,
other, white, white, other, other, white,
other, other, other, other, other, other, other, other,
other, other, other, other, other, other, other, other,
other, other, white,
punct, other, punct, punct, punct, punct, other, // !"#$%&´
brace, brace, punct, punct, comma, punct, punct, punct, // ()*+,-./
digit, digit, digit, digit, digit, // 01234
digit, digit, digit, digit, digit, // 56789
punct, punct, punct, punct, punct, punct, punct, // :;<=>?@
alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha, // ABCDEFGH
alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha,
alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha,
alpha, alpha, brace, punct, brace, punct, alpha, other, // YZ[\]^_'
alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha, // abcdefgh
alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha,
alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha,
alpha, alpha, brace, punct, brace, punct // yz{|}~
];
In that case, the formatter is smart enough to recognize this and preserve your original newlines. So, if you have a collection that you have carefully split into lines, add at least one line comment somewhere inside it to get it to preserve all of the newlines in it.
If that's not sufficient (and you are using the tall style), you can opt the collection out of automated formatting and format it yourself.
Initially, the formatter had a simple, restricted charter: rewrite only the non-semantic whitespace of your program. Make absolutely no other changes to your code. This makes it more reliable to run the formatter automatically in things like presubmit scripts where a human does not vet the output.
With the tall style, we have slightly loosened that. It adds and removes trailing commas. It sometimes moves comments before or after commas. Trailing commas and comments aren't whitespace, but they aren't semantic either. The code means exactly the same regardless of what you do with them.
If we're willing to make those kinds of changes, why not other ones? There are two main reasons why:
First, the formatter has a goal of being reversible. If you run the formatter periodically while making a series of code changes, the formatter should never leave detritus in the code that indicates when you happened to format it. Some examples:
-
If we add curlies to the body of an
if
that doesn't fit on one line, do we remove them when later it does fit? What if the user prefers using curly braces on allif
s? If we don't remove them, then it means the formatter's behavior isn't reversible. Say you make anif
condition longer and format, it may add curlies. Then you change it back to the original shorter condition. If the formatter doesn't remove curly braces, you aren't back where you started even before the code changes you made are. -
If we split long string literals so that they fit in the line length, do we unsplit adjacent ones that would fit? What kind of string literal do we use when we split or unsplit? How do we handle escaped quotation marks that are affected by that choice? Are all of the things we might do here reversible? Likewise with re-wrapping comments.
Second, some seemingly simple code changes can have subtle failure modes:
-
If we alphabetize your imports, what happens to comments in the middle of them? What if it appears to be a commented out import? Do we sort it?
-
If we change the delimiter characters of your strings, what rules do we use to choose between
'
and"
? Minimum number of escapes needed? Based on the contents of the string?
These kinds of questions mean these changes should have a human validate the result. If it doesn't do what you want, you want to know. But the formatter is often run every single time you save a file. Users usually save after they've validated that the code is what they expect. They don't tend to read it again after saving.
Thus, the formatter still has a fairly restricted charter for the kinds of
changes it makes. For more aggressive automated changes, you want dart fix
.