Skip to content

Commit f270828

Browse files
authored
Handle comments and metadata before variables more gracefully. (#1612)
Handle comments and metadata before variables more gracefully. Comments and metadata before a declaration are always tricky because the formatter by default attaches comments to the innermost piece following the comment. So in: ```dart // Comment int field; ``` We want the comment attached to the Piece for the entire field declaration. But the formatter won't see the comment until it hits the `int` token and will end up attaching it to the piece for the type annotation which is embedded inside the field piece. That in turn means that the formatter will think there is a newline inside the type (because there is one between the comment and `int`) which then forces a split after the type annotation too: ```dart // Comment int field; ``` In most places, this is handled by having the surrounding SequenceBuilder grab the comment before visiting the subsequent declaration. But that doesn't work when you have a comment after metadata: ```dart @meta // Comment int field; ``` Now the comment isn't before the field declaration. It's stuck inside it. But we still want to hoist it out. This PR fixes that for every place I could find: fields, functions, variables, and for-loop variables. The latter is particularly hard because for-in loops have some weird formatting already and it's also just a weird place for splits to occur. I wish I had a cleaner more systematic way of handling these but despite trying for most of today, I haven't been able to come up with a cleaner approach. This at least gets the formatter producing better output. Fix #1604.
1 parent 1208f9e commit f270828

11 files changed

+460
-26
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## 3.0.1-wip
22

33
* Ensure comment formatting is idempotent (#1606).
4+
* Handle comments and metadata before variables more gracefully (#1604).
45

56
## 3.0.0
67

lib/src/front_end/ast_node_visitor.dart

+6-6
Original file line numberDiff line numberDiff line change
@@ -765,32 +765,32 @@ final class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {
765765

766766
@override
767767
void visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) {
768-
throw UnsupportedError('This node is handled by createFor().');
768+
throw UnsupportedError('This node is handled by writeFor().');
769769
}
770770

771771
@override
772772
void visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) {
773-
throw UnsupportedError('This node is handled by createFor().');
773+
throw UnsupportedError('This node is handled by writeFor().');
774774
}
775775

776776
@override
777777
void visitForEachPartsWithPattern(ForEachPartsWithPattern node) {
778-
throw UnsupportedError('This node is handled by createFor().');
778+
throw UnsupportedError('This node is handled by writeFor().');
779779
}
780780

781781
@override
782782
void visitForPartsWithDeclarations(ForPartsWithDeclarations node) {
783-
throw UnsupportedError('This node is handled by createFor().');
783+
throw UnsupportedError('This node is handled by writeFor().');
784784
}
785785

786786
@override
787787
void visitForPartsWithExpression(ForPartsWithExpression node) {
788-
throw UnsupportedError('This node is handled by createFor().');
788+
throw UnsupportedError('This node is handled by writeFor().');
789789
}
790790

791791
@override
792792
void visitForPartsWithPattern(ForPartsWithPattern node) {
793-
throw UnsupportedError('This node is handled by createFor().');
793+
throw UnsupportedError('This node is handled by writeFor().');
794794
}
795795

796796
@override

lib/src/front_end/delimited_list_builder.dart

+5
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ final class DelimitedListBuilder {
138138
_commentsBeforeComma = CommentSequence.empty;
139139
}
140140

141+
/// Adds all of [pieces] to the built list.
142+
void addAll(Iterable<Piece> pieces) {
143+
pieces.forEach(add);
144+
}
145+
141146
/// Adds the contents of [lineBuilder] to this outer [DelimitedListBuilder].
142147
///
143148
/// This is used when preserving newlines inside a collection literal. The

lib/src/front_end/piece_factory.dart

+75-13
Original file line numberDiff line numberDiff line change
@@ -437,9 +437,16 @@ mixin PieceFactory {
437437
forPartsPiece = partsList.build();
438438

439439
case ForEachParts forEachParts &&
440-
ForEachPartsWithDeclaration(loopVariable: AstNode variable):
440+
ForEachPartsWithDeclaration(:var loopVariable):
441+
forPartsPiece = pieces.build(() {
442+
pieces.token(leftParenthesis);
443+
_writeDeclaredForIn(
444+
loopVariable, forEachParts.inKeyword, forEachParts.iterable);
445+
pieces.token(rightParenthesis);
446+
});
447+
441448
case ForEachParts forEachParts &&
442-
ForEachPartsWithIdentifier(identifier: AstNode variable):
449+
ForEachPartsWithIdentifier(:var identifier):
443450
// If a for-in loop, treat the for parts like an assignment, so they
444451
// split like:
445452
//
@@ -481,7 +488,8 @@ mixin PieceFactory {
481488
// statement or element.
482489
forPartsPiece = pieces.build(() {
483490
pieces.token(leftParenthesis);
484-
writeForIn(variable, forEachParts.inKeyword, forEachParts.iterable);
491+
_writeForIn(
492+
identifier, forEachParts.inKeyword, forEachParts.iterable);
485493
pieces.token(rightParenthesis);
486494
});
487495

@@ -490,14 +498,23 @@ mixin PieceFactory {
490498
forPartsPiece = pieces.build(() {
491499
pieces.token(leftParenthesis);
492500

501+
// Hoist any leading comments so they don't force the for-in clauses
502+
// to split.
503+
var leadingComments = const <Piece>[];
504+
if (metadata.isEmpty) {
505+
leadingComments = pieces.takeCommentsBefore(keyword);
506+
}
507+
493508
// Use a nested piece so that the metadata precedes the keyword and
494509
// not the `(`.
495-
pieces.withMetadata(metadata, inlineMetadata: true, () {
510+
var forInPiece =
511+
pieces.build(metadata: metadata, inlineMetadata: true, () {
496512
pieces.token(keyword);
497513
pieces.space();
498-
499-
writeForIn(pattern, forEachParts.inKeyword, forEachParts.iterable);
514+
_writeForIn(pattern, forEachParts.inKeyword, forEachParts.iterable);
500515
});
516+
517+
pieces.add(prependLeadingComments(leadingComments, forInPiece));
501518
pieces.token(rightParenthesis);
502519
});
503520
}
@@ -1399,21 +1416,66 @@ mixin PieceFactory {
13991416
canBlockSplitRight: canBlockSplitRight));
14001417
}
14011418

1402-
/// Writes a [Piece] for the `<variable> in <expression>` part of a for-in
1403-
/// loop.
1404-
void writeForIn(AstNode leftHandSide, Token inKeyword, Expression sequence) {
1419+
/// Writes the `<variable> in <expression>` part of an identifier or pattern
1420+
/// for-in loop.
1421+
void _writeForIn(AstNode leftHandSide, Token inKeyword, Expression sequence) {
1422+
// Hoist any leading comments so they don't force the for-in clauses to
1423+
// split.
1424+
var leadingComments =
1425+
pieces.takeCommentsBefore(leftHandSide.firstNonCommentToken);
1426+
14051427
var leftPiece =
14061428
nodePiece(leftHandSide, context: NodeContext.forLoopVariable);
1429+
var sequencePiece = _createForInSequence(inKeyword, sequence);
1430+
1431+
pieces.add(prependLeadingComments(
1432+
leadingComments,
1433+
ForInPiece(leftPiece, sequencePiece,
1434+
canBlockSplitSequence: sequence.canBlockSplit)));
1435+
}
1436+
1437+
/// Writes the `<variable> in <expression>` part of a for-in loop when the
1438+
/// part before `in` is a variable declaration.
1439+
///
1440+
/// A for-in loop with a variable declaration can have metadata before it,
1441+
/// which requires some special handling so that we don't push the metadata
1442+
/// and any comments after it into the left child piece of [ForInPiece].
1443+
void _writeDeclaredForIn(
1444+
DeclaredIdentifier identifier, Token inKeyword, Expression sequence) {
1445+
// Hoist any leading comments so they don't force the for-in clauses
1446+
// to split.
1447+
var leadingComments = const <Piece>[];
1448+
if (identifier.metadata.isEmpty) {
1449+
leadingComments = pieces
1450+
.takeCommentsBefore(identifier.firstTokenAfterCommentAndMetadata);
1451+
}
1452+
1453+
// Use a nested piece so that the metadata precedes the keyword and
1454+
// not the `(`.
1455+
var forInPiece =
1456+
pieces.build(metadata: identifier.metadata, inlineMetadata: true, () {
1457+
var leftPiece = pieces.build(() {
1458+
writeParameter(
1459+
modifiers: [identifier.keyword], identifier.type, identifier.name);
1460+
});
14071461

1408-
var sequencePiece = pieces.build(() {
1462+
var sequencePiece = _createForInSequence(inKeyword, sequence);
1463+
1464+
pieces.add(ForInPiece(leftPiece, sequencePiece,
1465+
canBlockSplitSequence: sequence.canBlockSplit));
1466+
});
1467+
1468+
pieces.add(prependLeadingComments(leadingComments, forInPiece));
1469+
}
1470+
1471+
/// Creates a piece for the `in <sequence>` part of a for-in loop.
1472+
Piece _createForInSequence(Token inKeyword, Expression sequence) {
1473+
return pieces.build(() {
14091474
// Put the `in` at the beginning of the sequence.
14101475
pieces.token(inKeyword);
14111476
pieces.space();
14121477
pieces.visit(sequence);
14131478
});
1414-
1415-
pieces.add(ForInPiece(leftPiece, sequencePiece,
1416-
canBlockSplitSequence: sequence.canBlockSplit));
14171479
}
14181480

14191481
/// Writes a piece for a parameter-like constructor: Either a simple formal

lib/src/front_end/piece_writer.dart

+12-5
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,18 @@ final class PieceWriter {
177177
_flushSpace();
178178
_currentCode = null;
179179

180-
var metadataPieces = const <Piece>[];
180+
var leadingPieces = const <Piece>[];
181181
if (metadata.isNotEmpty) {
182-
metadataPieces = [
182+
leadingPieces = [
183183
for (var annotation in metadata) _visitor.nodePiece(annotation)
184184
];
185+
186+
// If there are comments between the metadata and declaration, then hoist
187+
// them out too so they don't get embedded inside the beginning piece of
188+
// the declaration. [SequenceBuilder] handles that for most comments
189+
// preceding a declaration but won't see these ones because they come
190+
// after the metadata.
191+
leadingPieces.addAll(takeCommentsBefore(metadata.last.endToken.next!));
185192
}
186193

187194
_pieces.add([]);
@@ -198,7 +205,7 @@ final class PieceWriter {
198205
? builtPieces.first
199206
: AdjacentPiece(builtPieces);
200207

201-
if (metadataPieces.isEmpty) {
208+
if (leadingPieces.isEmpty) {
202209
// No metadata, so return the content piece directly.
203210
return builtPiece;
204211
} else if (inlineMetadata) {
@@ -210,7 +217,7 @@ final class PieceWriter {
210217
spaceWhenUnsplit: true,
211218
));
212219

213-
for (var piece in metadataPieces) {
220+
for (var piece in leadingPieces) {
214221
list.add(piece);
215222
}
216223

@@ -219,7 +226,7 @@ final class PieceWriter {
219226
} else {
220227
// Wrap the metadata and content in a sequence.
221228
var sequence = SequenceBuilder(_visitor);
222-
for (var piece in metadataPieces) {
229+
for (var piece in leadingPieces) {
223230
sequence.add(piece);
224231
}
225232

test/tall/declaration/metadata_comment.unit

+97-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,100 @@ class C {}
1616
'DatabaseCallback',
1717
) // deprecated
1818
@Experimental()
19-
class C {}
19+
class C {}
20+
>>> Comment between metadata and field doesn't force type to split.
21+
class C {
22+
@meta
23+
// c
24+
var a;
25+
26+
@meta
27+
// c
28+
int b;
29+
30+
@meta
31+
// c
32+
final int c;
33+
34+
@meta
35+
// c
36+
late var d;
37+
38+
@meta
39+
// c
40+
late int e;
41+
42+
@meta
43+
// c
44+
late final int f;
45+
46+
@meta
47+
// c
48+
static var g;
49+
50+
@meta
51+
// c
52+
static int h;
53+
54+
@meta
55+
// c
56+
static final int y;
57+
}
58+
<<<
59+
class C {
60+
@meta
61+
// c
62+
var a;
63+
64+
@meta
65+
// c
66+
int b;
67+
68+
@meta
69+
// c
70+
final int c;
71+
72+
@meta
73+
// c
74+
late var d;
75+
76+
@meta
77+
// c
78+
late int e;
79+
80+
@meta
81+
// c
82+
late final int f;
83+
84+
@meta
85+
// c
86+
static var g;
87+
88+
@meta
89+
// c
90+
static int h;
91+
92+
@meta
93+
// c
94+
static final int y;
95+
}
96+
>>> Comment between metadata and method doesn't force return type to split.
97+
class C {
98+
@meta
99+
// c
100+
int a() {}
101+
102+
@meta
103+
// c
104+
static int b() {}
105+
}
106+
<<<
107+
class C {
108+
@meta
109+
// c
110+
int a() {}
111+
112+
@meta
113+
// c
114+
static int b() {}
115+
}

test/tall/regression/1600/1604.unit

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
>>>
2+
class C {
3+
@override
4+
// ignore: hash_and_equals
5+
final int hashCode;
6+
}
7+
<<<
8+
class C {
9+
@override
10+
// ignore: hash_and_equals
11+
final int hashCode;
12+
}
File renamed without changes.

0 commit comments

Comments
 (0)