Skip to content

Commit be7eb5e

Browse files
authored
For loop metadata (#1406)
* Add DelimitedListBuilder.addLeftBracket(). The existing leftBracket() method takes a couple of optional parameters to build up a Piece in different ways. But an upcoming change also adds yet another way that the left bracket Piece might need to be constructed. Instead of adding yet another optional parameter, I just made a separate addLeftBracket() method that takes a complete Piece made any way you want and then changed the existing callsites to use that unless they only need a single token. * Change the Piece structure for Object patterns. This change is mainly to ensure that every outermost Piece for a pattern that can be block-formatted is a ListPiece. That will be needed in a later commit so that when a pattern appears to the left of an `=`, we can constrain it to block split sometimes. But this change also slightly changes the formatting of object patterns in a way that I think looks better: if the type arguments in an object pattern split, it forces the fields to split too. * Turn createForLoopParts() into createFor(). The only two calls createForLoopParts() are in visitForStatement() and visitForElement() and those two are very similar. In a future commit, they will have more code to share, so reorganizing now to make it easier to share it. * Move the operator in AssignPiece to a separate child piece. Prior to this change, the operator was directly attached to either the LHS piece (for ` =`) or the RHS (for `in `). This pulls it out to its own piece. The formatting is unchanged by this commit. In a future commit, we'll use this to be able to constrain the LHS child piece without the operator getting in the way. * Rework AssignPiece formatting. Formatting assignments is complex because LHS can be block split, or expression split, or not split. Likewise with the RHS, and thus all nine combinations. Even figuring out what the formatting should be is tricky. The "in" clause of a for-in loop also uses the same formatting logic, which adds more complexity because now it's nested inside a surrounding statement-like form and parentheses. It gets even weirder when you have metadata before the variable (which isn't in this commit). This change tweaks the formatting rules to, as best as I can tell, make the edge cases look as nice as I can get. It also adds more tests for corners that weren't covered. The main changes are: - If the RHS of an `in` expression splits, then go ahead and split before the `in` too. It's pretty rare for splits in `in` to occur in the wild, but when they do, the Flutter tends to keep it tightly packed with the rest the for loop variable. The initial style tried to follow that. But it gets really weird if you happen to have metadata in there. I also don't love how it looks. Usually, when a split occurs in an operand or clause, we split at the preceding operator or keyword too. This commit does that. If an expression split occurs to the right of `in`, it splits before the `in` too. - Tweak the relative priority between block splitting on the LHS of an assignment versus at the operator itself. The main way this is visible is that `=>` function bodies (which also use AssignPiece) now prefer to split after the `=>` instead of in the parameter list. I think that looks better. - If the LHS *can* block split but chooses not to, then don't allow the RHS to expression split. This is the most important tweak. Before this change, the formatter could produce: for (var [i] in veryLongIterator + longExpression) { ; } That was never intended. The `in` shouldn't get buried in the middle of a line like that. This change fixes that by having the AssignPiece explicitly constrain the LHS to split when it's a block and it's going to allow the RHS to expression split. * Format metadata on for loop variables. This is the actual *new* syntax supported by this PR. * Allow splitting between the type and name in a for loop variable. This was an oversight. All other type annotated variables allow splitting between the type and name (even though it's rare). * Consistently call buildPiece() directly in the argument to addLeftBracket(). The other callsites all do, so may as well do so here too.
1 parent a6ad769 commit be7eb5e

16 files changed

+552
-164
lines changed

lib/src/front_end/ast_node_visitor.dart

Lines changed: 88 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import '../piece/adjacent_strings.dart';
1414
import '../piece/assign.dart';
1515
import '../piece/case.dart';
1616
import '../piece/constructor.dart';
17-
import '../piece/for.dart';
1817
import '../piece/if.dart';
1918
import '../piece/infix.dart';
2019
import '../piece/list.dart';
@@ -375,8 +374,12 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
375374
Piece? initializerSeparator;
376375
Piece? initializers;
377376
if (node.redirectedConstructor case var constructor?) {
378-
redirect = AssignPiece(
379-
tokenPiece(node.separator!), nodePiece(constructor),
377+
var separator = buildPiece((b) {
378+
b.token(node.separator);
379+
b.space();
380+
});
381+
382+
redirect = AssignPiece(separator, nodePiece(constructor),
380383
canBlockSplitRight: false);
381384
} else if (node.initializers.isNotEmpty) {
382385
initializerSeparator = tokenPiece(node.separator!);
@@ -436,11 +439,11 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
436439

437440
@override
438441
Piece visitDeclaredIdentifier(DeclaredIdentifier node) {
439-
return buildPiece((b) {
440-
b.modifier(node.keyword);
441-
b.visit(node.type, spaceAfter: true);
442-
b.token(node.name);
443-
});
442+
return createParameter(
443+
metadata: node.metadata,
444+
modifiers: [node.keyword],
445+
node.type,
446+
node.name);
444447
}
445448

446449
@override
@@ -521,7 +524,13 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
521524
this,
522525
const ListStyle(
523526
spaceWhenUnsplit: true, splitListIfBeforeSplits: true));
524-
builder.leftBracket(node.leftBracket, preceding: header);
527+
528+
builder.addLeftBracket(buildPiece((b) {
529+
b.add(header);
530+
b.space();
531+
b.token(node.leftBracket);
532+
}));
533+
525534
node.constants.forEach(builder.visit);
526535
builder.rightBracket(semicolon: node.semicolon, node.rightBracket);
527536
metadataBuilder.add(builder.build());
@@ -677,11 +686,13 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
677686

678687
// If all parameters are optional, put the `[` or `{` right after `(`.
679688
var builder = DelimitedListBuilder(this);
680-
if (node.parameters.isNotEmpty && firstOptional == 0) {
681-
builder.leftBracket(node.leftParenthesis, delimiter: node.leftDelimiter);
682-
} else {
683-
builder.leftBracket(node.leftParenthesis);
684-
}
689+
690+
builder.addLeftBracket(buildPiece((b) {
691+
b.token(node.leftParenthesis);
692+
if (node.parameters.isNotEmpty && firstOptional == 0) {
693+
b.token(node.leftDelimiter);
694+
}
695+
}));
685696

686697
for (var i = 0; i < node.parameters.length; i++) {
687698
// If this is the first optional parameter, put the delimiter before it.
@@ -698,70 +709,57 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
698709

699710
@override
700711
Piece visitForElement(ForElement node) {
701-
var forKeyword = buildPiece((b) {
702-
b.modifier(node.awaitKeyword);
703-
b.token(node.forKeyword);
704-
});
705-
706-
var forPartsPiece = createForLoopParts(
707-
node.leftParenthesis, node.forLoopParts, node.rightParenthesis);
708-
var body = nodePiece(node.body);
709-
710-
var forPiece = ForPiece(forKeyword, forPartsPiece, body,
711-
hasBlockBody: node.body.isSpreadCollection);
712-
713-
// It looks weird to have multiple nested control flow elements on the same
714-
// line, so force the outer one to split if there is an inner one.
715-
if (node.body.isControlFlowElement) {
716-
forPiece.pin(State.split);
717-
}
718-
719-
return forPiece;
712+
return createFor(
713+
awaitKeyword: node.awaitKeyword,
714+
forKeyword: node.forKeyword,
715+
leftParenthesis: node.leftParenthesis,
716+
forLoopParts: node.forLoopParts,
717+
rightParenthesis: node.rightParenthesis,
718+
body: node.body,
719+
hasBlockBody: node.body.isSpreadCollection,
720+
forceSplitBody: node.body.isControlFlowElement);
720721
}
721722

722723
@override
723724
Piece visitForStatement(ForStatement node) {
724-
var forKeyword = buildPiece((b) {
725-
b.modifier(node.awaitKeyword);
726-
b.token(node.forKeyword);
727-
});
728-
729-
var forPartsPiece = createForLoopParts(
730-
node.leftParenthesis, node.forLoopParts, node.rightParenthesis);
731-
var body = nodePiece(node.body);
732-
733-
return ForPiece(forKeyword, forPartsPiece, body,
725+
return createFor(
726+
awaitKeyword: node.awaitKeyword,
727+
forKeyword: node.forKeyword,
728+
leftParenthesis: node.leftParenthesis,
729+
forLoopParts: node.forLoopParts,
730+
rightParenthesis: node.rightParenthesis,
731+
body: node.body,
734732
hasBlockBody: node.body is Block);
735733
}
736734

737735
@override
738736
Piece visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) {
739-
throw UnsupportedError('This node is handled by visitForStatement().');
737+
throw UnsupportedError('This node is handled by createFor().');
740738
}
741739

742740
@override
743741
Piece visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) {
744-
throw UnsupportedError('This node is handled by visitForStatement().');
742+
throw UnsupportedError('This node is handled by createFor().');
745743
}
746744

747745
@override
748746
Piece visitForEachPartsWithPattern(ForEachPartsWithPattern node) {
749-
throw UnsupportedError('This node is handled by visitForStatement().');
747+
throw UnsupportedError('This node is handled by createFor().');
750748
}
751749

752750
@override
753751
Piece visitForPartsWithDeclarations(ForPartsWithDeclarations node) {
754-
throw UnsupportedError('This node is handled by visitForStatement().');
752+
throw UnsupportedError('This node is handled by createFor().');
755753
}
756754

757755
@override
758756
Piece visitForPartsWithExpression(ForPartsWithExpression node) {
759-
throw UnsupportedError('This node is handled by visitForStatement().');
757+
throw UnsupportedError('This node is handled by createFor().');
760758
}
761759

762760
@override
763761
Piece visitForPartsWithPattern(ForPartsWithPattern node) {
764-
throw UnsupportedError('This node is handled by visitForStatement().');
762+
throw UnsupportedError('This node is handled by createFor().');
765763
}
766764

767765
@override
@@ -1297,14 +1295,16 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
12971295

12981296
@override
12991297
Piece visitObjectPattern(ObjectPattern node) {
1300-
return buildPiece((b) {
1298+
var builder = DelimitedListBuilder(this);
1299+
1300+
builder.addLeftBracket(buildPiece((b) {
13011301
b.visit(node.type);
1302-
b.add(createCollection(
1303-
node.leftParenthesis,
1304-
node.fields,
1305-
node.rightParenthesis,
1306-
));
1307-
});
1302+
b.token(node.leftParenthesis);
1303+
}));
1304+
1305+
node.fields.forEach(builder.visit);
1306+
builder.rightBracket(node.rightParenthesis);
1307+
return builder.build();
13081308
}
13091309

13101310
@override
@@ -1384,7 +1384,13 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
13841384
@override
13851385
Piece visitPatternVariableDeclaration(PatternVariableDeclaration node) {
13861386
return buildPiece((b) {
1387-
b.metadata(node.metadata);
1387+
// If the variable is part of a for loop, it looks weird to force the
1388+
// metadata to split since it's in a sort of expression-ish location:
1389+
//
1390+
// for (@meta var (x, y) in pairs) ...
1391+
b.metadata(node.metadata,
1392+
inline: node.parent is ForEachPartsWithPattern ||
1393+
node.parent is ForPartsWithPattern);
13881394
b.token(node.keyword);
13891395
b.space();
13901396
b.add(createAssignment(node.pattern, node.equals, node.expression));
@@ -1481,14 +1487,12 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
14811487
var builder = DelimitedListBuilder(this, listStyle);
14821488

14831489
// If all parameters are optional, put the `{` right after `(`.
1484-
if (positionalFields.isEmpty && namedFields != null) {
1485-
builder.leftBracket(
1486-
node.leftParenthesis,
1487-
delimiter: namedFields.leftBracket,
1488-
);
1489-
} else {
1490-
builder.leftBracket(node.leftParenthesis);
1491-
}
1490+
builder.addLeftBracket(buildPiece((b) {
1491+
b.token(node.leftParenthesis);
1492+
if (positionalFields.isEmpty && namedFields != null) {
1493+
b.token(namedFields.leftBracket);
1494+
}
1495+
}));
14921496

14931497
for (var positionalField in positionalFields) {
14941498
builder.visit(positionalField);
@@ -1678,7 +1682,12 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
16781682

16791683
var list = DelimitedListBuilder(this,
16801684
const ListStyle(spaceWhenUnsplit: true, splitListIfBeforeSplits: true));
1681-
list.leftBracket(node.leftBracket, preceding: value);
1685+
1686+
list.addLeftBracket(buildPiece((b) {
1687+
b.add(value);
1688+
b.space();
1689+
b.token(node.leftBracket);
1690+
}));
16821691

16831692
for (var member in node.cases) {
16841693
list.visit(member);
@@ -1830,7 +1839,12 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
18301839
@override
18311840
Piece visitVariableDeclarationList(VariableDeclarationList node) {
18321841
var header = buildPiece((b) {
1833-
b.metadata(node.metadata);
1842+
// If the variable is part of a for loop, it looks weird to force the
1843+
// metadata to split since it's in a sort of expression-ish location:
1844+
//
1845+
// for (@meta var x in list) ...
1846+
b.metadata(node.metadata,
1847+
inline: node.parent is ForPartsWithDeclarations);
18341848
b.modifier(node.lateKeyword);
18351849
b.modifier(node.keyword);
18361850

@@ -1844,15 +1858,19 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
18441858
for (var variable in node.variables) {
18451859
if ((variable.equals, variable.initializer)
18461860
case (var equals?, var initializer?)) {
1847-
var variablePiece = buildPiece((b) {
1848-
b.token(variable.name);
1861+
var variablePiece = tokenPiece(variable.name);
1862+
1863+
var equalsPiece = buildPiece((b) {
18491864
b.space();
18501865
b.token(equals);
18511866
});
18521867

18531868
var initializerPiece = nodePiece(initializer, commaAfter: true);
18541869

1855-
variables.add(AssignPiece(variablePiece, initializerPiece,
1870+
variables.add(AssignPiece(
1871+
left: variablePiece,
1872+
equalsPiece,
1873+
initializerPiece,
18561874
canBlockSplitRight: initializer.canBlockSplit));
18571875
} else {
18581876
variables.add(tokenPiece(variable.name, commaAfter: true));

lib/src/front_end/delimited_list_builder.dart

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,23 +61,13 @@ class DelimitedListBuilder {
6161
}
6262

6363
/// Adds the opening [bracket] to the built list.
64-
///
65-
/// If [delimiter] is given, it is a second bracket occurring immediately
66-
/// after [bracket]. This is used for parameter lists where all parameters
67-
/// are optional or named, as in:
68-
///
69-
/// function([parameter]);
70-
///
71-
/// Here, [bracket] will be `(` and [delimiter] will be `[`.
72-
void leftBracket(Token bracket, {Piece? preceding, Token? delimiter}) {
73-
_leftBracket = _visitor.buildPiece((b) {
74-
if (preceding != null) {
75-
b.add(preceding);
76-
b.space();
77-
}
78-
b.token(bracket);
79-
b.token(delimiter);
80-
});
64+
void leftBracket(Token bracket) {
65+
addLeftBracket(_visitor.tokenPiece(bracket));
66+
}
67+
68+
/// Adds the opening bracket [piece] to the built list.
69+
void addLeftBracket(Piece piece) {
70+
_leftBracket = piece;
8171
}
8272

8373
/// Adds the closing [bracket] to the built list along with any comments that

0 commit comments

Comments
 (0)