Skip to content
Bob Nystrom edited this page Nov 18, 2024 · 11 revisions

Why have a formatter?

The formatter has a few goals, in order of descending priority:

  1. 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.

  2. 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.

  3. 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.

  4. 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 formatted 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;

I don't like the output!

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 need to get back to a happy place.

How do I configure the page width?

Line wrapping is one of the most important and most challenging things the formatter does. By default, it will try to keep every line of code 80 characters long or shorter. We've found this to be a good width uses screen space well, plays nice with side-by-side diff tools, and avoids long difficult to read lines.

However, some users strongly prefer different page sizes. You can configure this using an analysis_options.yaml file. In that file, add a top-level section like:

formatter:
  page_width: 123

When you run dart format, it will look for an analysis_options.yaml file in the directory where each formatted file is found. If not found there, it will walk parent directories until it finds one. This way, you can set the page width for an entire collection of files with a single options file. Typically, you have a single analysis_options.yaml file in the root directory of your pub package and it applies to everything in that package.

If no analysis_options.yaml is found, then dart format defaults to 80 columns.

You can override this on a per-file basis by adding a comment like this to the top of a file:

// dart format width=123

This comment must appear before any code in the file and must match that format exactly. The width set by the comment overrides the width set by any surrounding analysis_options.yaml file.

This feature is mainly for code generators that generate and immediately format code but don't know about any surrounding analysis_options.yaml that might be configuring the page width. By inserting this comment in the generated code before formatting, it ensures that the code generator's behavior matches the behavior of dart format.

End users should mostly use analysis_options.yaml for configuring their preferred page width (or do nothing and use the default page width of 80).

Why can't I configure it in other ways?

The formatter supports very few tweakable settings, by design. If you look up at the list of priorities above, you'll see configurability goes directly against the first two priorities, and halfway against the third (you have to think about it, but not apply it).

This may be surprising, but the goal of dart format is not to automatically make your code look the way you like. It's to make everyone's Dart code look the same. The primary goal of dart format is to improve the quality of the Dart ecosystem. That transitively improves the live's of each Dart developer as well—you get more code to reuse, more consistent code, it's easier to read and contribute to each other's code, etc.

But it does that at the expense of individual preference. It requires you to buy in to the idea that a consistent ecosystem is more valuable than anyone's individual formatting preferences. Or, another way to say that is that no one's individual formatting style is measurably better enough than what dart format produces to compensate for the costs of inconsistency and having to argue over what your team's house style should be.

If you don't buy that, that's OK. It just means dart format probably isn't the right tool for you.

If it's not configurable, why are there two styles?

For over a decade, the formatter supported one single opinioned style. That style was older than Flutter, older than many Dart language features, and older than almost all Dart code in the world today.

The way users write Dart code has changed significantly in those years. In particular, Flutter uses Dart as sort of a UI markup language, leading to large deeply nested declarative expressions. The formatter's original style was optimizing for the kind of imperative code that was more common in Dart's early history, but doesn't work as well for deep tree-like expressions.

When support for trailing commas was later added to Dart, it wasn't clear how they should be formatted. Up to that point, the formatter always placed the closing ) of an argument list right after the last argument. It's silly to do that if there's a trailing comma stuck in there, so the hacky compromise we added at the time was that if you write a trailing comma, you get a different formatting style:

// Without trailing comma:
someFunction(longArgument,
    anotherArgument);

// With trailing comma:
someFunction(
  longArgument,
  anotherArgument,
);

This hack undermined the formatter's main goal of getting users out of the business of making tiny meaningless formatting choices. But it did let us observe which style users preferred in the wild. Over time (and with surveys and analysis of huge corpora of Dart to back it up), it became clear that most users prefer the latter style.

So we rewrote the formatter internally to support two styles, the original "short" style, and a new "tall" style that always formats like the latter argument list and adds and removes trailing commas on your behalf.

As of 2024, we are in the process of migrating the ecosystem over to that new style. We use the Dart language version of the code being formatted to determine which style you get. Code at Dart 3.6 or earlier continues to be formatted using the old style so that users don't see their code formatting spontaneously change under them. Code at Dart 3.7 or later gets the new style. When you update your pubspec's SDK constraint to >=3.7 or later, you are also opting in to the new style.

The goal is not to support both styles indefinitely. We still want a single ecosystem with a single consistent style. We are supporting both for some amount of time to ease the transition and let users migrate when it's a good time for them to do so. At some point in the future, we'll remove support for the old style completely and all code will be formatted using the tall style, regardless of language version.

https://github.com/dart-lang/dart_style/issues/1253

How stable is it?

You can rely on the formatter to not break your code or change its semantics. If it does do so, 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. We don't want to cause unnecessary churn to all of that code without good reason.

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 be settle down fairly quickly.

In general, we try to keep the formatting style stable to minimize churn.

How does it work?

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.

How do I tell the formatter to ignore a region of code?

If your code is using the tall style, you can opt a region of code out of automated formatting by surrounding it in a pair of special marker comments:

main() {
  this.isFormatted();

  // dart format off
  no   +   formatting
    +
      here;
  // dart format on

  formatting.isBackOnHere();
}

The comments must be exactly // dart format off and // dart format on. A file may have multiple regions, but they can't overlap or nest. If you want to opt the rest of a file out, you can use a single // dart format off comment without a closing one at the end.

Disabling formatting can be useful for highly structured data where custom layout can help a reader understand the data, like large lists of numbers. But, in general, we recommend you use this feature sparingly. When you opt code out of automated formatting, you are opting back in to all of the headaches that manual formatting entails: arguing with teammates about style, needing to manually reformat it after applying automated refactoring, reduced readability for users who expect a different style, etc.

Why are function and collection literals inside argument lists formatted weird?

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 argument lists should be formatted in this style 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 it's very limited understanding of your code.

Why does the formatter mess up my collection literals?

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.

Why doesn't the formatter make other style changes?

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. Commas and comments aren't whitespace, but they aren't semantic either. The code means exactly the same either way.

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:

[reversible]: https://github.com/dart-lang/dart_style/wiki/Reversibility-principle)

  • If we add curlies to the body of an if that doesn't fit on one line, do we remove them if later it does fit? What if the user prefers using curly braces on all ifs? If we don't remove them, then it means the formatter's behavior isn't reversible. Say you make an if 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 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.

To that end, the formatter still has a fairly restricted charter for the kinds of changes it makes. For more aggressive automated changes, try dart fix.

Clone this wiki locally