Skip to content

Commit 029f9d3

Browse files
authored
Format for-in loops. (#1314)
Format for-in loops. I ended up refactoring the code for handling the for parts and inlining it all into visitForStatement() because it turns out that there isn't as much sharing between C-style for loops and for-in loops as I was expecting. Thanks to the magic of pattern matching and shared case bodies, I was still able to get it all into a single flattened switch statement for all the kinds of for parts.
1 parent cf1c1c2 commit 029f9d3

File tree

4 files changed

+211
-88
lines changed

4 files changed

+211
-88
lines changed

lib/src/front_end/ast_node_visitor.dart

Lines changed: 95 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,9 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
339339

340340
@override
341341
void visitDeclaredIdentifier(DeclaredIdentifier node) {
342-
throw UnimplementedError();
342+
modifier(node.keyword);
343+
visit(node.type, after: space);
344+
token(node.name);
343345
}
344346

345347
@override
@@ -481,66 +483,114 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
481483

482484
@override
483485
void visitForStatement(ForStatement node) {
486+
modifier(node.awaitKeyword);
484487
token(node.forKeyword);
485488
var forKeyword = pieces.split();
486489

487-
// Treat the for loop parts sort of as an argument list where each clause
488-
// is a separate argument. This means that when they split, they split like:
489-
//
490-
// ```
491-
// for (
492-
// initializerClause;
493-
// conditionClause;
494-
// incrementClause
495-
// ) {
496-
// body;
497-
// }
498-
// ```
499-
var partsList =
500-
DelimitedListBuilder(this, const ListStyle(commas: Commas.none));
501-
partsList.leftBracket(node.leftParenthesis);
502-
503-
var forLoopParts = node.forLoopParts;
504-
switch (forLoopParts) {
490+
Piece forPartsPiece;
491+
switch (node.forLoopParts) {
505492
// Edge case: A totally empty for loop is formatted just as `(;;)` with
506493
// no splits or spaces anywhere.
507494
case ForPartsWithExpression(
508-
initialization: null,
509-
leftSeparator: Token(precedingComments: null),
510-
condition: null,
511-
rightSeparator: Token(precedingComments: null),
512-
updaters: NodeList(isEmpty: true),
513-
)
495+
initialization: null,
496+
leftSeparator: Token(precedingComments: null),
497+
condition: null,
498+
rightSeparator: Token(precedingComments: null),
499+
updaters: NodeList(isEmpty: true),
500+
) &&
501+
var forParts
514502
when node.rightParenthesis.precedingComments == null:
515-
token(forLoopParts.leftSeparator);
516-
token(forLoopParts.rightSeparator);
503+
token(node.leftParenthesis);
504+
token(forParts.leftSeparator);
505+
token(forParts.rightSeparator);
506+
token(node.rightParenthesis);
507+
forPartsPiece = pieces.split();
508+
509+
case ForParts forParts &&
510+
ForPartsWithDeclarations(variables: AstNode? initializer):
511+
case ForParts forParts &&
512+
ForPartsWithExpression(initialization: AstNode? initializer):
513+
// In a C-style for loop, treat the for loop parts like an argument list
514+
// where each clause is a separate argument. This means that when they
515+
// split, they split like:
516+
//
517+
// ```
518+
// for (
519+
// initializerClause;
520+
// conditionClause;
521+
// incrementClause
522+
// ) {
523+
// body;
524+
// }
525+
// ```
526+
var partsList =
527+
DelimitedListBuilder(this, const ListStyle(commas: Commas.none));
528+
partsList.leftBracket(node.leftParenthesis);
529+
530+
// The initializer clause.
531+
if (initializer != null) {
532+
partsList.addCommentsBefore(initializer.beginToken);
533+
visit(initializer);
534+
} else {
535+
// No initializer, so look at the comments before `;`.
536+
partsList.addCommentsBefore(forParts.leftSeparator);
537+
}
538+
539+
token(forParts.leftSeparator);
540+
partsList.add(pieces.split());
541+
542+
// The condition clause.
543+
if (forParts.condition case var conditionExpression?) {
544+
partsList.addCommentsBefore(conditionExpression.beginToken);
545+
visit(conditionExpression);
546+
} else {
547+
partsList.addCommentsBefore(forParts.rightSeparator);
548+
}
517549

518-
case ForPartsWithDeclarations():
519-
partsList.addCommentsBefore(forLoopParts.variables.beginToken);
520-
visit(forLoopParts.variables);
521-
finishForParts(forLoopParts, partsList);
550+
token(forParts.rightSeparator);
551+
partsList.add(pieces.split());
522552

523-
case ForPartsWithExpression(:var initialization?):
524-
partsList.addCommentsBefore(initialization.beginToken);
525-
visit(initialization);
526-
finishForParts(forLoopParts, partsList);
553+
// The update clauses.
554+
if (forParts.updaters.isNotEmpty) {
555+
partsList.addCommentsBefore(forParts.updaters.first.beginToken);
556+
createList(forParts.updaters,
557+
style: const ListStyle(commas: Commas.nonTrailing));
558+
partsList.add(pieces.split());
559+
}
527560

528-
case ForPartsWithExpression():
529-
// No initializer part.
530-
partsList.addCommentsBefore(forLoopParts.leftSeparator);
531-
finishForParts(forLoopParts, partsList);
561+
partsList.rightBracket(node.rightParenthesis);
562+
pieces.split();
563+
forPartsPiece = partsList.build();
532564

533565
case ForPartsWithPattern():
534-
case ForEachPartsWithDeclaration():
535-
case ForEachPartsWithIdentifier():
566+
throw UnimplementedError();
567+
568+
case ForEachParts forEachParts &&
569+
ForEachPartsWithDeclaration(loopVariable: AstNode variable):
570+
case ForEachParts forEachParts &&
571+
ForEachPartsWithIdentifier(identifier: AstNode variable):
572+
// If a for-in loop, treat the for parts like an assignment, so they
573+
// split like:
574+
//
575+
// ```
576+
// for (var variable in [
577+
// initializer,
578+
// ]) {
579+
// body;
580+
// }
581+
// ```
582+
token(node.leftParenthesis);
583+
visit(variable);
584+
585+
finishAssignment(forEachParts.inKeyword, forEachParts.iterable,
586+
splitBeforeOperator: true);
587+
token(node.rightParenthesis);
588+
forPartsPiece = pieces.split();
589+
536590
case ForEachPartsWithPattern():
537591
throw UnimplementedError();
538592
}
539593

540-
partsList.rightBracket(node.rightParenthesis);
541-
var forPartsPiece = partsList.build();
542-
543-
pieces.split();
544594
visit(node.body);
545595
var body = pieces.take();
546596

lib/src/front_end/piece_factory.dart

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -561,22 +561,38 @@ mixin PieceFactory implements CommentWriter {
561561
if (body.keyword != null) space();
562562
}
563563

564-
/// Creates a [Piece] for some code followed by an `=` and an expression in
565-
/// any place where an `=` appears:
564+
/// Creates a [Piece] with "assignment-like" splitting.
565+
///
566+
/// This is used, obviously, for assignments and variable declarations to
567+
/// handle splitting after the `=`, but is also used in any context where an
568+
/// expression follows something that it "defines" or "initializes":
566569
///
567570
/// * Assignment
568571
/// * Variable declaration
569572
/// * Constructor initializer
573+
/// * Expression (`=>`) function body
574+
/// * Named argument or named record field (`:`)
575+
/// * Map entry (`:`)
576+
/// * For-in loop iterator (`in`)
570577
///
571-
/// This is also used for map literal entries and named arguments which are
572-
/// also sort of like bindings. In that case, [operator] is the `:`.
573-
///
574-
/// This method assumes the code to the left of the `=` or `:` has already
578+
/// This method assumes the code to the left of the operator has already
575579
/// been visited.
576-
void finishAssignment(Token operator, Expression rightHandSide) {
577-
if (operator.type == TokenType.EQ) space();
578-
token(operator);
579-
var target = pieces.split();
580+
///
581+
/// If [splitBeforeOperator] is `true`, then puts [operator] at the beginning
582+
/// of the next line when it splits. Otherwise, puts the operator at the end
583+
/// of the preceding line.
584+
void finishAssignment(Token operator, Expression rightHandSide,
585+
{bool splitBeforeOperator = false}) {
586+
Piece target;
587+
if (splitBeforeOperator) {
588+
target = pieces.split();
589+
token(operator);
590+
space();
591+
} else {
592+
if (operator.type == TokenType.EQ) space();
593+
token(operator);
594+
target = pieces.split();
595+
}
580596

581597
visit(rightHandSide);
582598

@@ -594,32 +610,6 @@ mixin PieceFactory implements CommentWriter {
594610
rightBracket: argumentList.rightParenthesis);
595611
}
596612

597-
/// Writes the condition and updaters parts of a [ForParts] after the
598-
/// subclass's initializer clause has been written.
599-
void finishForParts(ForParts forLoopParts, DelimitedListBuilder partsList) {
600-
token(forLoopParts.leftSeparator);
601-
partsList.add(pieces.split());
602-
603-
// The condition clause.
604-
if (forLoopParts.condition case var conditionExpression?) {
605-
partsList.addCommentsBefore(conditionExpression.beginToken);
606-
visit(conditionExpression);
607-
} else {
608-
partsList.addCommentsBefore(forLoopParts.rightSeparator);
609-
}
610-
611-
token(forLoopParts.rightSeparator);
612-
partsList.add(pieces.split());
613-
614-
// The update clauses.
615-
if (forLoopParts.updaters.isNotEmpty) {
616-
partsList.addCommentsBefore(forLoopParts.updaters.first.beginToken);
617-
createList(forLoopParts.updaters,
618-
style: const ListStyle(commas: Commas.nonTrailing));
619-
partsList.add(pieces.split());
620-
}
621-
}
622-
623613
/// Finishes writing a named function declaration or anonymous function
624614
/// expression after the return type (if any) and name (if any) has been
625615
/// written.

lib/src/piece/assign.dart

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import 'piece.dart';
1010
///
1111
/// This piece is also used for map entries and named arguments where `:` is
1212
/// followed by an expression or element because those also want to support the
13-
/// "block-like" formatting of delimited expressions on the right.
13+
/// "block-like" formatting of delimited expressions on the right, and for the
14+
/// `in` clause in for-in loops.
1415
///
1516
/// These constructs can be formatted three ways:
1617
///
@@ -42,18 +43,21 @@ class AssignPiece extends Piece {
4243
/// This is only allowed when the value is a delimited expression.
4344
static const State _insideValue = State(1);
4445

45-
/// Split after the `=` and allow splitting inside the value.
46+
/// Split at the operator and allow splitting inside the value.
4647
///
4748
/// This is more costly because, when it's possible to split inside a
4849
/// delimited value, we want to prefer that.
49-
static const State _atEquals = State(2, cost: 2);
50+
static const State _atOperator = State(2, cost: 2);
5051

51-
/// The left-hand side of the `=` and the `=` itself.
52+
/// The left-hand side of the operation. Includes the operator unless it is
53+
/// `in`.
5254
final Piece target;
5355

54-
/// The right-hand side of the `=`.
56+
/// The right-hand side of the operation.
5557
final Piece value;
5658

59+
/// Whether the right-hand side is a delimited expression that should receive
60+
/// block-like formatting.
5761
final bool _isValueDelimited;
5862

5963
AssignPiece(this.target, this.value, {required bool isValueDelimited})
@@ -104,7 +108,7 @@ class AssignPiece extends Piece {
104108

105109
@override
106110
List<State> get additionalStates =>
107-
[if (_isValueDelimited) _insideValue, _atEquals];
111+
[if (_isValueDelimited) _insideValue, _atOperator];
108112

109113
@override
110114
void format(CodeWriter writer, State state) {
@@ -116,7 +120,7 @@ class AssignPiece extends Piece {
116120
if (state != _insideValue) writer.setIndent(Indent.expression);
117121

118122
writer.format(target);
119-
writer.splitIf(state == _atEquals);
123+
writer.splitIf(state == _atOperator);
120124
writer.format(value);
121125
}
122126

test/statement/for_in.stmt

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
40 columns |
2+
>>> Declare variable with `var`.
3+
for ( var file in files ) { body ; }
4+
<<<
5+
for (var file in files) {
6+
body;
7+
}
8+
>>> Declare with type annotation.
9+
for ( List < int > ints in intLists ) { body ; }
10+
<<<
11+
for (List<int> ints in intLists) {
12+
body;
13+
}
14+
>>> Declare variable with `final` and type.
15+
for (final Foo foo in foos) { body; }
16+
<<<
17+
for (final Foo foo in foos) {
18+
body;
19+
}
20+
>>> Declare variable with just `final`.
21+
for (final foo in foos) {
22+
body;
23+
}
24+
<<<
25+
for (final foo in foos) {
26+
body;
27+
}
28+
>>> Use existing variable.
29+
for ( x in things ) { body; }
30+
<<<
31+
for (x in things) {
32+
body;
33+
}
34+
>>> Await for with declared variable.
35+
foo() async {
36+
await for ( var x in y ) { body ; }
37+
}
38+
<<<
39+
foo() async {
40+
await for (var x in y) {
41+
body;
42+
}
43+
}
44+
>>> Await for with existing variable.
45+
foo() async {
46+
await for ( x in y ) { body ; }
47+
}
48+
<<<
49+
foo() async {
50+
await for (x in y) {
51+
body;
52+
}
53+
}
54+
>>> Split before `in`.
55+
for (var identifier in iteratableExpression) { body; }
56+
<<<
57+
for (var identifier
58+
in iteratableExpression) {
59+
body;
60+
}
61+
>>> Prefer block-like splitting after `in`.
62+
for (var identifier in [element, element, element]) { body; }
63+
<<<
64+
for (var identifier in [
65+
element,
66+
element,
67+
element,
68+
]) {
69+
body;
70+
}
71+
>>> Unsplit non-block body.
72+
for (i in sequence) something(i);
73+
<<<
74+
for (i in sequence) something(i);
75+
>>> Split non-block body.
76+
for (i in sequence) somethingMuchLonger(i);
77+
<<<
78+
for (i in sequence)
79+
somethingMuchLonger(i);

0 commit comments

Comments
 (0)