Skip to content

Commit 100db45

Browse files
authored
Language version all of the formatting changes since Dart 3.7. (#1707)
Language version all of the formatting changes since Dart 3.7. The new formatter launch was pretty rocky, but one thing I think helped mitigate that was that the formatting style was gated on language version. Users could install the new SDK and their code wouldn't be spontaneously reformatted under them until they deliberately updated their SDK constraint to opt in to the new language version. This PR applies that same logic to the changes we've made since Dart 3.7. Any source file at language version 3.7 is formatted (almost, see below) identically to how it would be formatted with the SDK as it shipped in 3.7. Source files at language version 3.8 or higher get all of the new style changes I've landed since then. This increases the complexity of the formatter codebase a decent amount, but it's mostly just a lot of "if 3.7 do X else do Y". Tedious but not intractable. I expect to continue to language version changes like this going forward. So after Dart 3.8 ships, whatever formatter style is in that version will be locked down and style changes after that will have to support 3.7 and 3.8 fallback behavior. I expect there to be much less style churn going forward, so it's probably manageable. In cases where there are *bugs* (i.e. the formatter produces invalid syntax or rearranges code), those won't be language versioned. But most other style changes will be. It's important to make sure we don't regress and accidentally affect the formatting of older language versioned files when making changes to the formatter. To avoid that, this also expands testing to be multi-versioned. Every test is now run on multiple versions and for cases where the style differs between versions, there are different expectations for each. Fortunately, the tests run very fast, so it doesn't slow down things more than a couple of seconds. In addition to running through the formatter's own test suite and regression tests, I also tested this on a giant corpus of pub packages. I formatted them all using the shipped Dart 3.7 formatter, then using this PR with `--language-version=3.7`. Of the 89,468 files, 7 had differences. They all relate to a single weird corner case around giant multiline strings containing multiline string interpolation expressions where the formatting is slightly different. #1706 helps that somewhat -- there were about dozen diffs before -- but doesn't totally eliminate it. I think it's close enough to be tolerable.
1 parent c7f6131 commit 100db45

File tree

110 files changed

+3667
-502
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+3667
-502
lines changed

CHANGELOG.md

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,32 @@
11
## 3.1.0-wip
22

3-
* Format null-aware elements.
3+
### Features
4+
5+
* Allow preserving trailing commas and forcing the surrounding construct to
6+
split even when it would otherwise fit on one line. This is off by default
7+
(because it breaks [reversibility][] among other reasons) but can be enabled
8+
by adding this to a surrounding `analysis_options.yaml` file:
9+
10+
```yaml
11+
formatter:
12+
trailing_commas: preserve
13+
```
14+
15+
This is similar to how trailing commas work in the old short style formatter
16+
applied to code before language version 3.7.
17+
18+
[reversibility]: https://github.com/dart-lang/dart_style/wiki/Reversibility-principle
19+
20+
### Bug fixes
21+
22+
* Don't add a trailing comma in lists that don't allow it, even when there is
23+
a trailing comment (#1639).
24+
25+
### Style changes
26+
27+
The following style changes are language versioned and only affect code whose
28+
language version is 3.8 or later. Dart code at 3.7 or earlier is formatted the
29+
same as it was before.
430
531
* Allow more code on the same line as a named argument or `=>` (#1536, #1545,
632
#1668, #1679).
@@ -175,26 +201,6 @@
175201
);
176202
```
177203

178-
* Allow preserving trailing commas and forcing the surrounding construct to
179-
split even when it would otherwise fit on one line. This is off by default
180-
(because it breaks [reversibility][] among other reasons) but can be enabled
181-
by adding this to a surrounding `analysis_options.yaml` file:
182-
183-
```yaml
184-
formatter:
185-
trailing_commas: preserve
186-
```
187-
188-
This is similar to how trailing commas worked in the old short style
189-
formatter.
190-
191-
* Don't add a trailing comma in lists that don't allow it, even when there is
192-
a trailing comment (#1639).
193-
194-
* Add tests for digit separators.
195-
196-
[reversibility]: https://github.com/dart-lang/dart_style/wiki/Reversibility-principle
197-
198204
## 3.0.1
199205

200206
* Handle trailing commas in for-loop updaters (#1354).

example/format.dart

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:dart_style/dart_style.dart';
66
import 'package:dart_style/src/debug.dart' as debug;
77
import 'package:dart_style/src/testing/test_file.dart';
8+
import 'package:pub_semver/pub_semver.dart';
89

910
void main(List<String> args) {
1011
// Enable debugging so you can see some of the formatter's internal state.
@@ -32,29 +33,29 @@ void main(List<String> args) {
3233

3334
void _formatStmt(
3435
String source, {
35-
bool tall = true,
36+
Version? version,
3637
int pageWidth = 40,
3738
TrailingCommas trailingCommas = TrailingCommas.automate,
3839
}) {
3940
_runFormatter(
4041
source,
4142
pageWidth,
42-
tall: tall,
43+
version: version ?? DartFormatter.latestLanguageVersion,
4344
isCompilationUnit: false,
4445
trailingCommas: trailingCommas,
4546
);
4647
}
4748

4849
void _formatUnit(
4950
String source, {
50-
bool tall = true,
51+
Version? version,
5152
int pageWidth = 40,
5253
TrailingCommas trailingCommas = TrailingCommas.automate,
5354
}) {
5455
_runFormatter(
5556
source,
5657
pageWidth,
57-
tall: tall,
58+
version: version ?? DartFormatter.latestLanguageVersion,
5859
isCompilationUnit: true,
5960
trailingCommas: trailingCommas,
6061
);
@@ -63,16 +64,13 @@ void _formatUnit(
6364
void _runFormatter(
6465
String source,
6566
int pageWidth, {
66-
required bool tall,
67+
required Version version,
6768
required bool isCompilationUnit,
6869
TrailingCommas trailingCommas = TrailingCommas.automate,
6970
}) {
7071
try {
7172
var formatter = DartFormatter(
72-
languageVersion:
73-
tall
74-
? DartFormatter.latestLanguageVersion
75-
: DartFormatter.latestShortStyleLanguageVersion,
73+
languageVersion: version,
7674
pageWidth: pageWidth,
7775
trailingCommas: trailingCommas,
7876
);
@@ -110,19 +108,20 @@ Future<void> _runTest(
110108
var formatTest = testFile.tests.firstWhere((test) => test.line == line);
111109
var formatter = testFile.formatterForTest(formatTest);
112110

113-
var actual = formatter.formatSource(formatTest.input);
111+
var actual = formatter.formatSource(formatTest.input.code);
114112

115113
// The test files always put a newline at the end of the expectation.
116114
// Statements from the formatter (correctly) don't have that, so add
117115
// one to line up with the expected result.
118116
var actualText = actual.textWithSelectionMarkers;
119117
if (!testFile.isCompilationUnit) actualText += '\n';
120118

121-
var expectedText = formatTest.output.textWithSelectionMarkers;
119+
// TODO(rnystrom): Handle multiple outputs.
120+
var expectedText = formatTest.outputs.first.code.textWithSelectionMarkers;
122121

123-
print('$path ${formatTest.description}');
122+
print('$path ${formatTest.input.description}');
124123
_drawRuler('before', pageWidth);
125-
print(formatTest.input.textWithSelectionMarkers);
124+
print(formatTest.input.code.textWithSelectionMarkers);
126125
if (actualText == expectedText) {
127126
_drawRuler('result', pageWidth);
128127
print(actualText);

lib/src/ast_extensions.dart

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -420,22 +420,76 @@ extension AdjacentStringsExtensions on AdjacentStrings {
420420
/// To balance these, we omit the indentation in argument lists only if there
421421
/// are no other string arguments.
422422
bool get indentStrings {
423-
bool hasOtherStringArgument(List<Expression> arguments) => arguments.any(
424-
(argument) => argument != this && argument is StringLiteral,
425-
);
423+
return switch (parent) {
424+
ArgumentList(:var arguments) => _hasOtherStringArgument(arguments),
425+
426+
// Treat asserts like argument lists.
427+
Assertion(:var condition, :var message) => _hasOtherStringArgument([
428+
condition,
429+
if (message != null) message,
430+
]),
431+
432+
_ => true,
433+
};
434+
}
426435

436+
/// Whether subsequent strings should be indented relative to the first
437+
/// string (in 3.7 style).
438+
///
439+
/// We generally want to indent adjacent strings because it can be confusing
440+
/// otherwise when they appear in a list of expressions, like:
441+
///
442+
/// [
443+
/// "one",
444+
/// "two"
445+
/// "three",
446+
/// "four"
447+
/// ]
448+
///
449+
/// Especially when these strings are longer, it can be hard to tell that
450+
/// "three" is a continuation of the previous element.
451+
///
452+
/// However, the indentation is distracting in places that don't suffer from
453+
/// this ambiguity:
454+
///
455+
/// var description =
456+
/// "A very long description..."
457+
/// "this extra indentation is unnecessary.");
458+
///
459+
/// To balance these, we omit the indentation when an adjacent string
460+
/// expression is in a context where it's unlikely to be confusing.
461+
bool get indentStringsV37 {
427462
return switch (parent) {
428-
ArgumentList(:var arguments) => hasOtherStringArgument(arguments),
463+
ArgumentList(:var arguments) => _hasOtherStringArgument(arguments),
429464

430465
// Treat asserts like argument lists.
431-
Assertion(:var condition, :var message) => hasOtherStringArgument([
466+
Assertion(:var condition, :var message) => _hasOtherStringArgument([
432467
condition,
433468
if (message != null) message,
434469
]),
435470

471+
// Don't add extra indentation in a variable initializer or assignment:
472+
//
473+
// var variable =
474+
// "no extra"
475+
// "indent";
476+
VariableDeclaration() => false,
477+
AssignmentExpression(:var rightHandSide) when rightHandSide == this =>
478+
false,
479+
480+
// Don't indent when following `:`.
481+
MapLiteralEntry(:var value) when value == this => false,
482+
NamedExpression() => false,
483+
484+
// Don't indent when the body of a `=>` function.
485+
ExpressionFunctionBody() => false,
436486
_ => true,
437487
};
438488
}
489+
490+
bool _hasOtherStringArgument(List<Expression> arguments) => arguments.any(
491+
(argument) => argument != this && argument is StringLiteral,
492+
);
439493
}
440494

441495
extension PatternExtensions on DartPattern {
@@ -445,7 +499,7 @@ extension PatternExtensions on DartPattern {
445499
/// Whether this expression is a non-empty delimited container for inner
446500
/// expressions that allows "block-like" formatting in some contexts.
447501
///
448-
/// See [ExpressionExtensions.canBlockSplit].
502+
/// See [ExpressionExtensions38.canBlockSplit].
449503
bool get canBlockSplit => switch (this) {
450504
ConstantPattern(:var expression) => expression.canBlockSplit,
451505
ListPattern(:var elements, :var rightBracket) => elements.canSplit(

lib/src/back_end/code.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ final class GroupCode extends Code {
135135

136136
/// A [Code] object for a newline followed by any leading indentation.
137137
final class _NewlineCode extends Code {
138-
/// True if a blank line (two newlines) should be written.
138+
/// Whether a blank line (two newlines) should be written.
139139
final bool _blank;
140140

141141
/// The number of spaces of indentation after this newline.

0 commit comments

Comments
 (0)