Skip to content

Commit 2cee560

Browse files
Format switch statements and expressions. (#1294)
Format switch statements and expressions. Switch statements build on the existing SequencePiece but need some additional flexibility in there to handle the fact that switch case lines are indented +2 and then case body lines are indented +4. Switch expressions build on the existing ListPiece since the body is sort of like a collection literal. There are a few tweaks needed here too since an unsplit switch expression body gets spaces inside the curly brackets. Also, switch values (the part in the leading "( ... )") are formatted using ListPiece as well, since they are formatted essentially like a single-element argument list, except that they don't get a trailing comma. This does not implement all of the kinds of patterns that can appear in cases. It just implements basic constants needed for the tests. It also doesn't support guard clauses yet. Those will come later. Co-authored-by: Nate Bosch <[email protected]>
1 parent 874d511 commit 2cee560

File tree

12 files changed

+976
-121
lines changed

12 files changed

+976
-121
lines changed

lib/src/ast_extensions.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ extension ExpressionExtensions on Expression {
114114
ListLiteral() => true,
115115
MethodInvocation() => true,
116116
SetOrMapLiteral() => true,
117+
SwitchExpression() => true,
117118
// TODO(tall): Record literals.
118119
// TODO(tall): Instance creation expressions (`new` and `const`).
119-
// TODO(tall): Switch expressions.
120120
_ => false,
121121
};
122122

lib/src/front_end/ast_node_visitor.dart

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import 'package:analyzer/dart/ast/ast.dart';
55
import 'package:analyzer/dart/ast/visitor.dart';
66
import 'package:analyzer/source/line_info.dart';
77

8+
import '../constants.dart';
89
import '../dart_formatter.dart';
10+
import '../piece/block.dart';
911
import '../piece/do_while.dart';
1012
import '../piece/if.dart';
1113
import '../piece/infix.dart';
@@ -54,28 +56,28 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
5456

5557
if (node is CompilationUnit) {
5658
if (node.scriptTag case var scriptTag?) {
57-
sequence.add(scriptTag);
59+
sequence.visit(scriptTag);
5860
sequence.addBlank();
5961
}
6062

6163
// Put a blank line between the library tag and the other directives.
6264
Iterable<Directive> directives = node.directives;
6365
if (directives.isNotEmpty && directives.first is LibraryDirective) {
64-
sequence.add(directives.first);
66+
sequence.visit(directives.first);
6567
sequence.addBlank();
6668
directives = directives.skip(1);
6769
}
6870

6971
for (var directive in directives) {
70-
sequence.add(directive);
72+
sequence.visit(directive);
7173
}
7274

7375
for (var declaration in node.declarations) {
74-
sequence.add(declaration);
76+
sequence.visit(declaration);
7577
}
7678
} else {
7779
// Just formatting a single statement.
78-
sequence.add(node);
80+
sequence.visit(node);
7981
}
8082

8183
// Write any comments at the end of the code.
@@ -260,7 +262,8 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
260262

261263
@override
262264
void visitConstantPattern(ConstantPattern node) {
263-
throw UnimplementedError();
265+
if (node.constKeyword != null) throw UnimplementedError();
266+
visit(node.expression);
264267
}
265268

266269
@override
@@ -574,10 +577,10 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
574577
void visitLabeledStatement(LabeledStatement node) {
575578
var sequence = SequenceBuilder(this);
576579
for (var label in node.labels) {
577-
sequence.add(label);
580+
sequence.visit(label);
578581
}
579582

580-
sequence.add(node.statement);
583+
sequence.visit(node.statement);
581584
writer.push(sequence.build());
582585
}
583586

@@ -643,8 +646,8 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
643646
visit(node.methodName);
644647
visit(node.typeArguments);
645648

646-
createDelimited(node.argumentList.leftParenthesis,
647-
node.argumentList.arguments, node.argumentList.rightParenthesis);
649+
createList(node.argumentList.leftParenthesis, node.argumentList.arguments,
650+
node.argumentList.rightParenthesis);
648651
}
649652

650653
@override
@@ -927,17 +930,88 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
927930

928931
@override
929932
void visitSwitchExpression(SwitchExpression node) {
930-
throw UnimplementedError();
933+
var list = DelimitedListBuilder.switchBody(this);
934+
935+
createSwitchValue(node.switchKeyword, node.leftParenthesis, node.expression,
936+
node.rightParenthesis);
937+
writer.space();
938+
list.leftBracket(node.leftBracket);
939+
940+
for (var member in node.cases) {
941+
list.add(member);
942+
}
943+
944+
list.rightBracket(node.rightBracket);
945+
writer.push(list.build());
931946
}
932947

933948
@override
934949
void visitSwitchExpressionCase(SwitchExpressionCase node) {
935-
throw UnimplementedError();
950+
if (node.guardedPattern.whenClause != null) throw UnimplementedError();
951+
952+
visit(node.guardedPattern.pattern);
953+
writer.space();
954+
finishAssignment(node.arrow, node.expression);
936955
}
937956

938957
@override
939958
void visitSwitchStatement(SwitchStatement node) {
940-
throw UnimplementedError();
959+
createSwitchValue(node.switchKeyword, node.leftParenthesis, node.expression,
960+
node.rightParenthesis);
961+
962+
// Attach the ` {` after the `)` in the [ListPiece] created by
963+
// [createSwitchValue()].
964+
writer.space();
965+
token(node.leftBracket);
966+
writer.split();
967+
var switchPiece = writer.pop();
968+
969+
var sequence = SequenceBuilder(this);
970+
for (var member in node.members) {
971+
for (var label in member.labels) {
972+
sequence.visit(label);
973+
}
974+
975+
sequence.addCommentsBefore(member.keyword);
976+
token(member.keyword);
977+
978+
if (member is SwitchCase) {
979+
writer.space();
980+
visit(member.expression);
981+
} else if (member is SwitchPatternCase) {
982+
writer.space();
983+
984+
if (member.guardedPattern.whenClause != null) {
985+
throw UnimplementedError();
986+
}
987+
988+
visit(member.guardedPattern.pattern);
989+
} else {
990+
assert(member is SwitchDefault);
991+
// Nothing to do.
992+
}
993+
994+
token(member.colon);
995+
var casePiece = writer.pop();
996+
writer.split();
997+
998+
// Don't allow any blank lines between the `case` line and the first
999+
// statement in the case (or the next case if this case has no body).
1000+
sequence.add(casePiece, indent: Indent.none, allowBlankAfter: false);
1001+
1002+
for (var statement in member.statements) {
1003+
sequence.visit(statement, indent: Indent.block);
1004+
}
1005+
}
1006+
1007+
// Place any comments before the "}" inside the sequence.
1008+
sequence.addCommentsBefore(node.rightBracket);
1009+
1010+
token(node.rightBracket);
1011+
var rightBracketPiece = writer.pop();
1012+
1013+
writer.push(BlockPiece(switchPiece, sequence.build(), rightBracketPiece,
1014+
alwaysSplit: node.members.isNotEmpty));
9411015
}
9421016

9431017
@override
@@ -974,7 +1048,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
9741048

9751049
@override
9761050
void visitTypeArgumentList(TypeArgumentList node) {
977-
createDelimited(node.leftBracket, node.arguments, node.rightBracket);
1051+
createTypeList(node.leftBracket, node.arguments, node.rightBracket);
9781052
}
9791053

9801054
@override
@@ -989,7 +1063,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
9891063

9901064
@override
9911065
void visitTypeParameterList(TypeParameterList node) {
992-
createDelimited(node.leftBracket, node.typeParameters, node.rightBracket);
1066+
createTypeList(node.leftBracket, node.typeParameters, node.rightBracket);
9931067
}
9941068

9951069
@override

lib/src/front_end/delimited_list_builder.dart

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,81 @@ class DelimitedListBuilder {
3131

3232
late final Piece _rightBracket;
3333

34-
/// Whether this list is a list of type arguments or type parameters, versus
35-
/// any other kind of list.
34+
/// Whether this list should have a trailing comma if it splits.
3635
///
37-
/// Type arguments/parameters are different because:
36+
/// This is true for most lists but false for type parameters, type arguments,
37+
/// and switch values.
38+
final bool _trailingComma;
39+
40+
/// The cost of splitting this list. Normally 1, but higher for some lists
41+
/// that look worse when split.
42+
final int _splitCost;
43+
44+
/// Whether this list should have spaces inside the bracket when it doesn't
45+
/// split.
46+
///
47+
/// This is false for most lists, but true for switch expression bodies:
3848
///
39-
/// * The language doesn't allow a trailing comma in them.
40-
/// * Splitting in them looks aesthetically worse, so we increase the cost
41-
/// of doing so.
42-
bool _isTypeList = false;
49+
/// ```
50+
/// v = switch (e) { 1 => 'one', 2 => 'two' };
51+
/// // ^ ^
52+
/// ```
53+
final bool _spaceWhenUnsplit;
4354

44-
DelimitedListBuilder(this._visitor);
55+
/// Whether a split in the [_before] piece should force the list to split too.
56+
///
57+
/// See [ListPiece._splitListIfBeforeSplits] for more details.
58+
final bool _splitListIfBeforeSplits;
4559

4660
/// The list of comments following the most recently written element before
4761
/// any comma following the element.
4862
CommentSequence _commentsBeforeComma = CommentSequence.empty;
4963

64+
/// Creates a new [DelimitedListBuilder] for an argument list, collection
65+
/// literal, etc.
66+
DelimitedListBuilder(this._visitor)
67+
: _trailingComma = true,
68+
_splitCost = 1,
69+
_spaceWhenUnsplit = false,
70+
_splitListIfBeforeSplits = false;
71+
72+
/// Creates a new [DelimitedListBuilder] for a switch expression body.
73+
DelimitedListBuilder.switchBody(this._visitor)
74+
: _trailingComma = true,
75+
_splitCost = 1,
76+
_spaceWhenUnsplit = true,
77+
_splitListIfBeforeSplits = true;
78+
79+
/// Creates a new [DelimitedListBuilder] for the value part of a switch
80+
/// statement or expression:
81+
///
82+
/// ```
83+
/// switch (value) { ... }
84+
/// // ^^^^^^^
85+
/// ```
86+
DelimitedListBuilder.switchValue(this._visitor)
87+
: _trailingComma = false,
88+
_splitCost = 2,
89+
_spaceWhenUnsplit = false,
90+
_splitListIfBeforeSplits = false;
91+
92+
/// Creates a new [DelimitedListBuilder] for a type argument or type parameter
93+
/// list.
94+
DelimitedListBuilder.type(this._visitor)
95+
: _trailingComma = false,
96+
_splitCost = 2,
97+
_spaceWhenUnsplit = false,
98+
_splitListIfBeforeSplits = false;
99+
50100
ListPiece build() => ListPiece(
51-
_leftBracket, _elements, _blanksAfter, _rightBracket, _isTypeList);
101+
_leftBracket,
102+
_elements,
103+
_blanksAfter,
104+
_rightBracket,
105+
_splitCost,
106+
_trailingComma,
107+
_spaceWhenUnsplit,
108+
_splitListIfBeforeSplits);
52109

53110
/// Adds the opening [bracket] to the built list.
54111
///
@@ -66,9 +123,6 @@ class DelimitedListBuilder {
66123
_visitor.token(delimiter);
67124
_leftBracket = _visitor.writer.pop();
68125
_visitor.writer.split();
69-
70-
// No trailing commas in type argument and type parameter lists.
71-
if (bracket.type == TokenType.LT) _isTypeList = true;
72126
}
73127

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

lib/src/front_end/piece_factory.dart

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,13 @@ mixin PieceFactory implements CommentWriter {
5858
/// } else {}
5959
/// ```
6060
void createBlock(Block block, {bool forceSplit = false}) {
61-
// Edge case: If the block is completely empty, output it as simple
62-
// unsplittable text unless it's forced to split.
63-
if (block.statements.isEmptyBody(block.rightBracket) && !forceSplit) {
64-
token(block.leftBracket);
65-
token(block.rightBracket);
66-
return;
67-
}
68-
6961
token(block.leftBracket);
7062
var leftBracketPiece = writer.pop();
7163
writer.split();
7264

7365
var sequence = SequenceBuilder(this);
7466
for (var node in block.statements) {
75-
sequence.add(node);
67+
sequence.visit(node);
7668
}
7769

7870
// Place any comments before the "}" inside the block.
@@ -115,17 +107,7 @@ mixin PieceFactory implements CommentWriter {
115107
// The formatter will preserve the newline after element 3 and the lack of
116108
// them after the other elements.
117109

118-
createDelimited(leftBracket, elements, rightBracket);
119-
}
120-
121-
/// Creates a [ListPiece] for the given bracket-delimited set of elements.
122-
void createDelimited(
123-
Token leftBracket, Iterable<AstNode> elements, Token rightBracket) {
124-
var builder = DelimitedListBuilder(this);
125-
builder.leftBracket(leftBracket);
126-
elements.forEach(builder.add);
127-
builder.rightBracket(rightBracket);
128-
writer.push(builder.build());
110+
createList(leftBracket, elements, rightBracket);
129111
}
130112

131113
/// Creates metadata annotations for a directive.
@@ -373,6 +355,44 @@ mixin PieceFactory implements CommentWriter {
373355
writer.push(InfixPiece(operands));
374356
}
375357

358+
/// Creates a [ListPiece] for the given bracket-delimited set of elements.
359+
void createList(
360+
Token leftBracket, Iterable<AstNode> elements, Token rightBracket) {
361+
var builder = DelimitedListBuilder(this);
362+
builder.leftBracket(leftBracket);
363+
elements.forEach(builder.add);
364+
builder.rightBracket(rightBracket);
365+
writer.push(builder.build());
366+
}
367+
368+
/// Visits the `switch (expr)` part of a switch statement or expression.
369+
void createSwitchValue(Token switchKeyword, Token leftParenthesis,
370+
Expression value, Token rightParenthesis) {
371+
// Format like an argument list since it is an expression surrounded by
372+
// parentheses.
373+
var builder = DelimitedListBuilder.switchValue(this);
374+
375+
// Attach the `switch ` as part of the `(`.
376+
token(switchKeyword);
377+
writer.space();
378+
379+
builder.leftBracket(leftParenthesis);
380+
builder.add(value);
381+
builder.rightBracket(rightParenthesis);
382+
383+
writer.push(builder.build());
384+
}
385+
386+
/// Creates a [ListPiece] for a type argument or type parameter list.
387+
void createTypeList(
388+
Token leftBracket, Iterable<AstNode> elements, Token rightBracket) {
389+
var builder = DelimitedListBuilder.type(this);
390+
builder.leftBracket(leftBracket);
391+
elements.forEach(builder.add);
392+
builder.rightBracket(rightBracket);
393+
writer.push(builder.build());
394+
}
395+
376396
/// Writes the parts of a formal parameter shared by all formal parameter
377397
/// types: metadata, `covariant`, etc.
378398
void startFormalParameter(FormalParameter parameter) {

0 commit comments

Comments
 (0)