Skip to content

Commit c96a7a4

Browse files
authored
Format for statements. (#1296)
Format for statements. This handles: ``` for (initializer; condition; increment) ... for (var variable; condition; increment) ... ``` It doesn't add support for: ``` // No patterns yet: for (var [pattern]; condition; increment) ... // No for-each yet: for (existingVariable in iterable) ... for (var newVariable in iterable) ... // No for-each or patterns yet: for (var [pattern] in iterable) ... ``` This one was a lot of work. There were almost no tests for for loops already and the style approach I took here is different from the old style. Since the clauses are formatted more like a list now, I wanted to reuse DelimitedListBuilder but it took some work to get that playing nice with clauses that aren't comma separated. That's what the previous refactoring PR is to enable. After this, supporting for-each and for-with-patterns should be (I hope!) relatively straightforward. Took Nate's suggestion to use pattern matching for the edge case.
1 parent 32e54c5 commit c96a7a4

File tree

7 files changed

+713
-16
lines changed

7 files changed

+713
-16
lines changed

lib/src/front_end/ast_node_visitor.dart

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44
import 'package:analyzer/dart/ast/ast.dart';
5+
import 'package:analyzer/dart/ast/token.dart';
56
import 'package:analyzer/dart/ast/visitor.dart';
67
import 'package:analyzer/source/line_info.dart';
78

89
import '../constants.dart';
910
import '../dart_formatter.dart';
1011
import '../piece/block.dart';
1112
import '../piece/do_while.dart';
13+
import '../piece/for.dart';
1214
import '../piece/if.dart';
1315
import '../piece/infix.dart';
1416
import '../piece/list.dart';
@@ -425,37 +427,102 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
425427

426428
@override
427429
void visitForStatement(ForStatement node) {
428-
throw UnimplementedError();
430+
token(node.forKeyword);
431+
writer.split();
432+
var forKeyword = writer.pop();
433+
434+
// Treat the for loop parts sort of as an argument list where each clause
435+
// is a separate argument. This means that when they split, they split like:
436+
//
437+
// ```
438+
// for (
439+
// initializerClause;
440+
// conditionClause;
441+
// incrementClause
442+
// ) {
443+
// body;
444+
// }
445+
// ```
446+
var partsList =
447+
DelimitedListBuilder(this, const ListStyle(commas: Commas.none));
448+
partsList.leftBracket(node.leftParenthesis);
449+
450+
var forLoopParts = node.forLoopParts;
451+
switch (forLoopParts) {
452+
// Edge case: A totally empty for loop is formatted just as `(;;)` with
453+
// no splits or spaces anywhere.
454+
case ForPartsWithExpression(
455+
initialization: null,
456+
leftSeparator: Token(precedingComments: null),
457+
condition: null,
458+
rightSeparator: Token(precedingComments: null),
459+
updaters: NodeList(isEmpty: true),
460+
)
461+
when node.rightParenthesis.precedingComments == null:
462+
token(forLoopParts.leftSeparator);
463+
token(forLoopParts.rightSeparator);
464+
465+
case ForPartsWithDeclarations():
466+
partsList.addCommentsBefore(forLoopParts.variables.beginToken);
467+
visit(forLoopParts.variables);
468+
finishForParts(forLoopParts, partsList);
469+
470+
case ForPartsWithExpression(:var initialization?):
471+
partsList.addCommentsBefore(initialization.beginToken);
472+
visit(initialization);
473+
finishForParts(forLoopParts, partsList);
474+
475+
case ForPartsWithExpression():
476+
// No initializer part.
477+
partsList.addCommentsBefore(forLoopParts.leftSeparator);
478+
finishForParts(forLoopParts, partsList);
479+
480+
case ForPartsWithPattern():
481+
case ForEachPartsWithDeclaration():
482+
case ForEachPartsWithIdentifier():
483+
case ForEachPartsWithPattern():
484+
throw UnimplementedError();
485+
}
486+
487+
partsList.rightBracket(node.rightParenthesis);
488+
writer.split();
489+
var forPartsPiece = partsList.build();
490+
491+
visit(node.body);
492+
var body = writer.pop();
493+
494+
writer.push(ForPiece(forKeyword, forPartsPiece, body,
495+
hasBlockBody: node.body is Block));
429496
}
430497

431498
@override
432499
void visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) {
433-
throw UnimplementedError();
500+
assert(false, 'This node is handled by visitForStatement().');
434501
}
435502

436503
@override
437504
void visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) {
438-
throw UnimplementedError();
505+
assert(false, 'This node is handled by visitForStatement().');
439506
}
440507

441508
@override
442509
void visitForEachPartsWithPattern(ForEachPartsWithPattern node) {
443-
throw UnimplementedError();
510+
assert(false, 'This node is handled by visitForStatement().');
444511
}
445512

446513
@override
447514
void visitForPartsWithDeclarations(ForPartsWithDeclarations node) {
448-
throw UnimplementedError();
515+
assert(false, 'This node is handled by visitForStatement().');
449516
}
450517

451518
@override
452519
void visitForPartsWithExpression(ForPartsWithExpression node) {
453-
throw UnimplementedError();
520+
assert(false, 'This node is handled by visitForStatement().');
454521
}
455522

456523
@override
457524
void visitForPartsWithPattern(ForPartsWithPattern node) {
458-
throw UnimplementedError();
525+
assert(false, 'This node is handled by visitForStatement().');
459526
}
460527

461528
@override
@@ -647,8 +714,10 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
647714
visit(node.methodName);
648715
visit(node.typeArguments);
649716

650-
createList(node.argumentList.leftParenthesis, node.argumentList.arguments,
651-
node.argumentList.rightParenthesis);
717+
createList(
718+
leftBracket: node.argumentList.leftParenthesis,
719+
node.argumentList.arguments,
720+
rightBracket: node.argumentList.rightParenthesis);
652721
}
653722

654723
@override

lib/src/front_end/piece_factory.dart

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ mixin PieceFactory implements CommentWriter {
108108
// The formatter will preserve the newline after element 3 and the lack of
109109
// them after the other elements.
110110

111-
createList(leftBracket, elements, rightBracket);
111+
createList(leftBracket: leftBracket, elements, rightBracket: rightBracket);
112112
}
113113

114114
/// Creates metadata annotations for a directive.
@@ -357,13 +357,14 @@ mixin PieceFactory implements CommentWriter {
357357
}
358358

359359
/// Creates a [ListPiece] for the given bracket-delimited set of elements.
360-
void createList(
361-
Token leftBracket, Iterable<AstNode> elements, Token rightBracket,
362-
{ListStyle style = const ListStyle()}) {
360+
void createList(Iterable<AstNode> elements,
361+
{Token? leftBracket,
362+
Token? rightBracket,
363+
ListStyle style = const ListStyle()}) {
363364
var builder = DelimitedListBuilder(this, style);
364-
builder.leftBracket(leftBracket);
365+
if (leftBracket != null) builder.leftBracket(leftBracket);
365366
elements.forEach(builder.visit);
366-
builder.rightBracket(rightBracket);
367+
if (rightBracket != null) builder.rightBracket(rightBracket);
367368
writer.push(builder.build());
368369
}
369370

@@ -389,7 +390,10 @@ mixin PieceFactory implements CommentWriter {
389390
/// Creates a [ListPiece] for a type argument or type parameter list.
390391
void createTypeList(
391392
Token leftBracket, Iterable<AstNode> elements, Token rightBracket) {
392-
return createList(leftBracket, elements, rightBracket,
393+
return createList(
394+
leftBracket: leftBracket,
395+
elements,
396+
rightBracket: rightBracket,
393397
style: const ListStyle(commas: Commas.nonTrailing, splitCost: 2));
394398
}
395399

@@ -427,6 +431,34 @@ mixin PieceFactory implements CommentWriter {
427431
isValueDelimited: rightHandSide.isDelimited));
428432
}
429433

434+
/// Writes the condition and updaters parts of a [ForParts] after the
435+
/// subclass's initializer clause has been written.
436+
void finishForParts(ForParts forLoopParts, DelimitedListBuilder partsList) {
437+
token(forLoopParts.leftSeparator);
438+
writer.split();
439+
partsList.add(writer.pop());
440+
441+
// The condition clause.
442+
if (forLoopParts.condition case var conditionExpression?) {
443+
partsList.addCommentsBefore(conditionExpression.beginToken);
444+
visit(conditionExpression);
445+
} else {
446+
partsList.addCommentsBefore(forLoopParts.rightSeparator);
447+
}
448+
449+
token(forLoopParts.rightSeparator);
450+
writer.split();
451+
partsList.add(writer.pop());
452+
453+
// The update clauses.
454+
if (forLoopParts.updaters.isNotEmpty) {
455+
partsList.addCommentsBefore(forLoopParts.updaters.first.beginToken);
456+
createList(forLoopParts.updaters,
457+
style: const ListStyle(commas: Commas.nonTrailing));
458+
partsList.add(writer.pop());
459+
}
460+
}
461+
430462
/// Writes an optional modifier that precedes other code.
431463
void modifier(Token? keyword) {
432464
token(keyword, after: writer.space);

lib/src/piece/for.dart

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
import '../back_end/code_writer.dart';
5+
import '../constants.dart';
6+
import 'piece.dart';
7+
8+
/// A piece for a for statement.
9+
class ForPiece extends Piece {
10+
/// The `for` keyword.
11+
final Piece _forKeyword;
12+
13+
/// The part inside `( ... )`, including the parentheses themselves, at the
14+
/// header of a for statement.
15+
final Piece _forParts;
16+
17+
final Piece _body;
18+
19+
/// Whether the body of the loop is a block versus some other statement. If
20+
/// the body isn't a block, then we allow a discretionary split after the
21+
/// loop parts, as in:
22+
///
23+
/// ```
24+
/// for (;;)
25+
/// print("ok");
26+
/// ```
27+
final bool _hasBlockBody;
28+
29+
ForPiece(this._forKeyword, this._forParts, this._body,
30+
{required bool hasBlockBody})
31+
: _hasBlockBody = hasBlockBody;
32+
33+
/// If there is at least one else or else-if clause, then it always splits.
34+
@override
35+
List<State> get additionalStates => [if (!_hasBlockBody) State.split];
36+
37+
@override
38+
void format(CodeWriter writer, State state) {
39+
if (!_hasBlockBody && state == State.unsplit) {
40+
writer.setAllowNewlines(false);
41+
}
42+
43+
writer.format(_forKeyword);
44+
writer.space();
45+
writer.format(_forParts);
46+
47+
if (_hasBlockBody) {
48+
writer.space();
49+
} else {
50+
writer.splitIf(state == State.split, indent: Indent.block);
51+
}
52+
53+
writer.format(_body);
54+
}
55+
56+
@override
57+
void forEachChild(void Function(Piece piece) callback) {
58+
callback(_forKeyword);
59+
callback(_forParts);
60+
callback(_body);
61+
}
62+
63+
@override
64+
String toString() => 'For';
65+
}

test/statement/for.stmt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
40 columns |
2+
### Test for loop formatting that isn't affected by the for loop parts.
3+
>>> No clauses.
4+
for ( ; ; ) { body; }
5+
<<<
6+
for (;;) {
7+
body;
8+
}
9+
>>> Single line for body without braces.
10+
for (i = 0; i < 10; i) something(i);
11+
<<<
12+
for (i = 0; i < 10; i) something(i);
13+
>>> Split for body without braces.
14+
for (i = 0; i < 10; i) somethingLonger(i);
15+
<<<
16+
for (i = 0; i < 10; i)
17+
somethingLonger(i);

0 commit comments

Comments
 (0)