Skip to content

Commit 2af2bbc

Browse files
committed
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 2af2bbc

File tree

126 files changed

+3661
-559
lines changed

Some content is hidden

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

126 files changed

+3661
-559
lines changed

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_writer.dart

Lines changed: 99 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -154,31 +154,71 @@ final class CodeWriter {
154154
/// Increases the indentation by [indent] relative to the current amount of
155155
/// indentation.
156156
void pushIndent(Indent indent) {
157-
var parent = _indentStack.last;
158-
159-
// Combine the new indentation with the surrounding one.
160-
var offset = switch ((parent.type, indent)) {
161-
// On the right-hand side of `=`, `:`, or `=>`, don't indent subsequent
162-
// infix operands so that they all align:
163-
//
164-
// variable =
165-
// operand +
166-
// another;
167-
(Indent.assignment, Indent.infix) => 0,
168-
169-
// We have already indented the control flow header, so collapse the
170-
// duplicate indentation.
171-
(Indent.controlFlowClause, Indent.expression) => 0,
172-
(Indent.controlFlowClause, Indent.infix) => 0,
173-
174-
// If we get here, the parent context has no effect, so just apply the
175-
// indentation directly.
176-
(_, _) => indent.spaces,
177-
};
157+
if (_cache.isVersion37) {
158+
_pushIndentV37(indent);
159+
} else {
160+
var parent = _indentStack.last;
161+
162+
// Combine the new indentation with the surrounding one.
163+
var offset = switch ((parent.type, indent)) {
164+
// On the right-hand side of `=`, `:`, or `=>`, don't indent subsequent
165+
// infix operands so that they all align:
166+
//
167+
// variable =
168+
// operand +
169+
// another;
170+
(Indent.assignment, Indent.infix) => 0,
171+
172+
// We have already indented the control flow header, so collapse the
173+
// duplicate indentation.
174+
(Indent.controlFlowClause, Indent.expression) => 0,
175+
(Indent.controlFlowClause, Indent.infix) => 0,
176+
177+
// If we get here, the parent context has no effect, so just apply the
178+
// indentation directly.
179+
(_, _) => indent.spaces,
180+
};
181+
182+
_indentStack.add(_IndentLevel(indent, parent.spaces + offset));
183+
if (debug.traceIndent) {
184+
debug.log('pushIndent: ${_indentStack.join(' ')}');
185+
}
186+
}
187+
}
178188

179-
_indentStack.add(_IndentLevel(indent, parent.spaces + offset));
180-
if (debug.traceIndent) {
181-
debug.log('pushIndent: ${_indentStack.join(' ')}');
189+
/// Increases the indentation in a control flow clause in a "collapsible" way.
190+
///
191+
/// This is only used in a couple of corners of if-case and for-in headers
192+
/// where the indentation is unusual.
193+
void pushCollapsibleIndent() {
194+
if (_cache.isVersion37) {
195+
_pushIndentV37(Indent.expression, canCollapse: true);
196+
} else {
197+
pushIndent(Indent.controlFlowClause);
198+
}
199+
}
200+
201+
/// The 3.7 style of indentation and collapsible indentation tracking.
202+
void _pushIndentV37(Indent indent, {bool canCollapse = false}) {
203+
var parentIndent = _indentStack.last.spaces;
204+
var parentCollapse = _indentStack.last.collapsible;
205+
206+
if (parentCollapse == indent.spaces) {
207+
// We're indenting by the same existing collapsible amount, so collapse
208+
// this new indentation with that existing one.
209+
_indentStack.add(_IndentLevel.v37(parentIndent, 0));
210+
} else if (canCollapse) {
211+
// We should never get multiple levels of nested collapsible indentation.
212+
assert(parentCollapse == 0);
213+
214+
// Increase the indentation and note that it can be collapsed with
215+
// further indentation.
216+
_indentStack.add(
217+
_IndentLevel.v37(parentIndent + indent.spaces, indent.spaces),
218+
);
219+
} else {
220+
// Regular indentation, so just increase the indent.
221+
_indentStack.add(_IndentLevel.v37(parentIndent + indent.spaces, 0));
182222
}
183223
}
184224

@@ -230,7 +270,7 @@ final class CodeWriter {
230270
/// and multi-line strings.
231271
void whitespace(Whitespace whitespace, {bool flushLeft = false}) {
232272
if (whitespace case Whitespace.newline || Whitespace.blankLine) {
233-
_applyNewlineToShape();
273+
_applyNewlineToShape(_pieceFormats.last);
234274
_pendingIndent = flushLeft ? 0 : _indentStack.last.spaces;
235275
}
236276

@@ -261,11 +301,9 @@ final class CodeWriter {
261301
void format(Piece piece, {bool separate = false}) {
262302
if (separate) {
263303
Profile.count('CodeWriter.format() piece separate');
264-
265304
_formatSeparate(piece);
266305
} else {
267306
Profile.count('CodeWriter.format() piece inline');
268-
269307
_formatInline(piece);
270308
}
271309
}
@@ -328,23 +366,30 @@ final class CodeWriter {
328366
if (_pieceFormats.lastOrNull case var parent?) {
329367
var allowedShapes = parent.piece.allowedChildShapes(
330368
_solution.pieceState(parent.piece),
331-
piece,
369+
child.piece,
332370
);
333371

334-
if (!allowedShapes.contains(child.shape)) {
335-
_solution.invalidate(parent.piece);
336-
if (debug.traceSolver) {
337-
debug.log(
338-
'invalidate ${parent.piece} '
339-
'(${_solution.pieceState(parent.piece)}) '
340-
'because child $piece (${_solution.pieceState(piece)}) '
341-
'has ${child.shape} and allowed are $allowedShapes',
342-
);
343-
}
372+
bool invalid;
373+
if (_cache.isVersion37) {
374+
// If the child must be inline, then invalidate because we know it
375+
// contains some kind of newline.
376+
// TODO(rnystrom): It would be better if this logic wasn't different for
377+
// 3.7. The only place where the distinction between this code and the
378+
// logic in the else clause comes into play is with CaseExpressionPiece.
379+
invalid =
380+
child.shape != Shape.inline &&
381+
allowedShapes.length == 1 &&
382+
allowedShapes.contains(Shape.inline);
383+
} else {
384+
invalid = !allowedShapes.contains(child.shape);
344385
}
345386

387+
if (invalid) _solution.invalidate(parent.piece);
388+
346389
// If the child had newlines, propagate that to the parent's shape.
347-
if (child.shape != Shape.inline) _applyNewlineToShape(child.shape);
390+
if (child.shape != Shape.inline) {
391+
_applyNewlineToShape(parent, child.shape);
392+
}
348393
}
349394
}
350395

@@ -413,18 +458,17 @@ final class CodeWriter {
413458
}
414459

415460
/// Determine how a newline affects the current piece's shape.
416-
void _applyNewlineToShape([Shape shape = Shape.other]) {
417-
var format = _pieceFormats.last;
418-
format.shape = switch (format.mode) {
419-
ShapeMode.merge => format.shape.merge(shape),
461+
void _applyNewlineToShape(_FormatState state, [Shape shape = Shape.other]) {
462+
state.shape = switch (state.mode) {
463+
ShapeMode.merge => state.shape.merge(shape),
420464
ShapeMode.block => Shape.block,
421465
ShapeMode.beforeHeadline => Shape.other,
422466
// If there were no newlines inside the headline, now that there is one,
423467
// we have a headline shape.
424-
ShapeMode.afterHeadline when format.shape == Shape.inline =>
468+
ShapeMode.afterHeadline when state.shape == Shape.inline =>
425469
Shape.headline,
426470
// If there was already a newline in the headline, preserve that shape.
427-
ShapeMode.afterHeadline => format.shape,
471+
ShapeMode.afterHeadline => state.shape,
428472
ShapeMode.other => Shape.other,
429473
};
430474
}
@@ -555,12 +599,21 @@ enum ShapeMode {
555599
/// A level of indentation in the indentation stack.
556600
final class _IndentLevel {
557601
/// The reason this indentation was added.
602+
///
603+
/// Not used for 3.7 style.
558604
final Indent type;
559605

560606
/// The total number of spaces of indentation.
561607
final int spaces;
562608

563-
_IndentLevel(this.type, this.spaces);
609+
/// How many spaces of [spaces] can be collapsed with further indentation.
610+
///
611+
/// Only used for 3.7 style.
612+
final int collapsible;
613+
614+
_IndentLevel.v37(this.spaces, this.collapsible) : type = Indent.none;
615+
616+
_IndentLevel(this.type, this.spaces) : collapsible = 0;
564617

565618
@override
566619
String toString() => '${type.name}:$spaces';

0 commit comments

Comments
 (0)