Prefer to split call chains for single-element targets. (#1842)

Prefer to split call chains for single-element targets.

One of the most challenging parts of formatting Dart code well is dealing with method call chains on targets that can themselves split. The formatter needs to decide whether to split the target and avoid splitting the call chain or vice versa. For example:

```dart
// Split target:
function(
  argument,
).method().another();

// Or split chain:
function(argument)
  .method()
  .another();
```

Heuristics for that choice that look great on some callsites often make others look worse. I've toyed with many variations over the years.

This adds a fairly simple rule based on the examples in (#1732): If the target of a call chain only has *one* element or argument, then prefer to split the call chain and keep the target together. So in the above example, if prefers the second output. If `function()` had multiple arguments, it would prefer the first.

An intuitive argument in favor of this rule is that when we split a comma-separated argument or element list, what feels most intuitive to readers is splitting *between* the elements. Then as a secondary effect, we also split around the delimiting brackets to. If there is only one element, then there's no way to split between them, only around them. That doesn't look as good, so this style change avoids that.

Pragmatically, I think the results of the new rule look good. You can see a diff of running this on a random assortment of pub packages [here](https://gist.github.com/munificent/7d39fa6cd7667e8b9d9458638753e4f3).

This is a larger change than most these days. I ran it on a large corpus of files from Pub:

```
Affected / total files : 3,511 / 93,664 (~3.7%)
Affected / total lines : 35,770 / 22,424,287 (0.13%)
```

For affected lines, I'm ignoring additions and counting only the number of deleted lines from running `git diff --stat`. Since the formatter is always replacing a chunk of lines with another, I think that's a good way to measure the number of original lines which are impacted.

Fix #1732
19 files changed
tree: 15f9bbb6976e38918b67f40c2942c8831464aacc
  1. .github/
  2. benchmark/
  3. bin/
  4. dist/
  5. example/
  6. lib/
  7. test/
  8. tool/
  9. .gitignore
  10. analysis_options.yaml
  11. AUTHORS
  12. CHANGELOG.md
  13. LICENSE
  14. pubspec.yaml
  15. README.md
README.md

The dart_style package defines an opinionated, minimally configurable automated formatter for Dart code.

It replaces the whitespace in your program with what it deems to be the best formatting for it. It also makes minor changes around non-semantic punctuation like trailing commas and brackets in parameter lists.

The resulting code should follow the Dart style guide and look nice to most human readers, most of the time.

The formatter handles indentation, inline whitespace, and (by far the most difficult) intelligent line wrapping. It has no problems with nested collections, function expressions, long argument lists, or otherwise tricky code.

The formatter turns code like this:

process = await Process.start(path.join(p.pubCacheBinPath,Platform.isWindows
?'${command.first}.bat':command.first,),[...command.sublist(1),'web:0',
// Allow for binding to a random available port.
],workingDirectory:workingDir,environment:{'PUB_CACHE':p.pubCachePath,'PATH':
path.dirname(Platform.resolvedExecutable)+(Platform.isWindows?';':':')+
Platform.environment['PATH']!,},);

into:

process = await Process.start(
  path.join(
    p.pubCacheBinPath,
    Platform.isWindows ? '${command.first}.bat' : command.first,
  ),
  [
    ...command.sublist(1), 'web:0',
    // Allow for binding to a random available port.
  ],
  workingDirectory: workingDir,
  environment: {
    'PUB_CACHE': p.pubCachePath,
    'PATH':
        path.dirname(Platform.resolvedExecutable) +
        (Platform.isWindows ? ';' : ':') +
        Platform.environment['PATH']!,
  },
);

The formatter will never break your code—you can safely invoke it automatically from build and presubmit scripts.

Formatting files

The formatter is part of the unified dart developer tool included in the Dart SDK, so most users run it directly from there using dart format.

IDEs and editors that support Dart usually provide easy ways to run the formatter. For example, in Visual Studio Code, formatting Dart code will use the dart_style formatter by default. Most users have it set to reformat every time they save a file.

Here's a simple example of using the formatter on the command line:

$ dart format my_file.dart

This command formats the my_file.dart file and writes the result back to the same file.

The dart format command takes a list of paths, which can point to directories or files. If the path is a directory, it processes every .dart file in that directory and all of its subdirectories.

By default, dart format formats each file and writes the result back to the same files. If you pass --output show, it prints the formatted code to stdout and doesn't modify the files.

Validating formatting

If you want to use the formatter in something like a presubmit script or commit hook, you can pass flags to omit writing formatting changes to disk and to update the exit code to indicate success/failure:

$ dart format --output=none --set-exit-if-changed .

Using the formatter as a library

The dart_style package exposes a simple library API for formatting code. Basic usage looks like this:

import 'package:dart_style/dart_style.dart';

main() {
  final formatter = DartFormatter(
    languageVersion: DartFormatter.latestLanguageVersion,
  );

  try {
    print(formatter.format("""
    library an_entire_compilation_unit;

    class SomeClass {}
    """));

    print(formatter.formatStatement("aSingle(statement);"));
  } on FormatterException catch (ex) {
    print(ex);
  }
}

Other resources

  • Before sending an email, see if you are asking a frequently asked question.

  • Before filing a bug, or if you want to understand how work on the formatter is managed, see how we track issues.