Skip to content

Commit dd8b295

Browse files
authored
Format switch expressions, guards, and patterns in switches. (#1174)
* Format object patterns, const constructors, and constant expressions. The latter two were already implemented as part of keeping the existing switch tests passing, but this adds tests for their splitting and other behavior. For object patterns, I had to decide whether to split them like collections (like other patterns) or like argument lists (which they mirror). I went collection-like because they will appear mixed with other patterns. Also, if we migrate to a consistent flutter-like style at some point, it will be one less thing to change. Let me know what you think. * Format patterns in variables, assignments, and for loops. * Format switch expressions, guards, and patterns in switches. This also changes how regular cases in switch statements are formatted. Previously, a case's body was always moved to the next line: switch (obj) { case 1: body; case 2: body; } I honestly never really loved that style, but it worked OK. With switch expressions, it obviously makes sense to keep the expression inline if it fits: switch (obj) { 1 => body, 2 => body } I think people will generally prefer that style. I don't want them to use a switch expression where all they need is a switch statement just because it fits tighter, so I also allow switch statement case bodies to go next to the case if they fit now: switch (obj) { case 1: body; case 2: body; } I think this will be nice, especially since switch statements will likely become more common. Guards appear in three place: switch statement cases, switch expression cases, and if-case statements. There is little code reuse for how they are formatted in those three contexts. This is unfortunate, but deliberate. The indentation rules for guards are slightly different in all of those places because the surrounding context is different. I tried to make each context look as best as I could, which leads to unique formatting in each.
1 parent a468fb1 commit dd8b295

File tree

11 files changed

+850
-149
lines changed

11 files changed

+850
-149
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# 2.2.5-dev
22

33
* Format patterns and related features.
4+
* Allow switch statement case bodies on the same line as the case.
45
* Don't split after `<` in collection literals.
56
* Format record expressions and record type annotations.
67
* Better indentation of multiline function types inside type argument lists.

lib/src/nesting_builder.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@ class NestingBuilder {
8080
assert(_pendingNesting == null);
8181
assert(_nesting.indent == 0);
8282

83+
_stack.removeLast();
84+
8385
// If this fails, an unindent() call did not have a preceding indent() call.
8486
assert(_stack.isNotEmpty);
85-
86-
_stack.removeLast();
8787
}
8888

8989
/// Begins a new expression nesting level [indent] deeper than the current

lib/src/source_visitor.dart

Lines changed: 152 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,7 +1482,7 @@ class SourceVisitor extends ThrowingAstVisitor {
14821482
builder.endRule();
14831483
builder.unnest();
14841484

1485-
builder.nestExpression(indent: 2, now: true);
1485+
builder.nestExpression(indent: Indent.block, now: true);
14861486

14871487
if (isSpreadBody) {
14881488
space();
@@ -2747,73 +2747,147 @@ class SourceVisitor extends ThrowingAstVisitor {
27472747
}
27482748

27492749
@override
2750-
void visitSwitchCase(SwitchCase node) {
2751-
_visitLabels(node.labels);
2752-
token(node.keyword);
2753-
space();
2754-
visit(node.expression);
2755-
token(node.colon);
2750+
void visitSwitchExpression(SwitchExpression node) {
2751+
// Start the rule for splitting between the cases before the value. That
2752+
// way, if the value expression splits, the cases do too. Avoids:
2753+
//
2754+
// switch ([
2755+
// element,
2756+
// ]) { inline => caseBody };
2757+
builder.startRule();
27562758

2757-
builder.indent();
2758-
newline();
2759+
_visitSwitchValue(node.switchKeyword, node.leftParenthesis, node.expression,
2760+
node.rightParenthesis);
27592761

2760-
visitNodes(node.statements, between: oneOrTwoNewlines);
2761-
builder.unindent();
2762+
token(node.leftBracket);
2763+
builder = builder.startBlock(space: true);
2764+
2765+
visitCommaSeparatedNodes(node.cases, between: split);
2766+
2767+
var hasTrailingComma = node.cases.last.commaAfter != null;
2768+
_endBody(node.rightBracket, forceSplit: hasTrailingComma);
27622769
}
27632770

27642771
@override
2765-
void visitSwitchDefault(SwitchDefault node) {
2766-
_visitLabels(node.labels);
2767-
token(node.keyword);
2768-
token(node.colon);
2772+
void visitSwitchExpressionCase(SwitchExpressionCase node) {
2773+
// Wrap the rule for splitting after "=>" around the pattern so that a
2774+
// split in the pattern forces the expression to move to the next line too.
2775+
builder.startLazyRule();
27692776

2770-
builder.indent();
2771-
newline();
2777+
// Wrap the expression's nesting around the pattern too so that a split in
2778+
// the pattern is indented farther then the body expression. Used +2 indent
2779+
// because switch expressions are block-like, similar to how we split the
2780+
// bodies of if and for elements in collections.
2781+
builder.nestExpression(indent: Indent.block);
27722782

2773-
visitNodes(node.statements, between: oneOrTwoNewlines);
2774-
builder.unindent();
2783+
var whenClause = node.guardedPattern.whenClause;
2784+
if (whenClause == null) {
2785+
visit(node.guardedPattern.pattern);
2786+
} else {
2787+
// Wrap the when clause rule around the pattern so that if the pattern
2788+
// splits then we split before "when" too.
2789+
builder.startRule();
2790+
builder.nestExpression(indent: Indent.block);
2791+
visit(node.guardedPattern.pattern);
2792+
split();
2793+
builder.startBlockArgumentNesting();
2794+
_visitWhenClause(whenClause);
2795+
builder.endBlockArgumentNesting();
2796+
builder.unnest();
2797+
builder.endRule();
2798+
}
2799+
2800+
space();
2801+
token(node.arrow);
2802+
split();
2803+
2804+
builder.endRule();
2805+
2806+
visit(node.expression);
2807+
builder.unnest();
27752808
}
27762809

27772810
@override
2778-
void visitSwitchPatternCase(SwitchPatternCase node) {
2779-
_visitLabels(node.labels);
2811+
void visitSwitchStatement(SwitchStatement node) {
2812+
_visitSwitchValue(node.switchKeyword, node.leftParenthesis, node.expression,
2813+
node.rightParenthesis);
2814+
_beginBody(node.leftBracket);
27802815

2781-
token(node.keyword);
2782-
space();
2816+
for (var member in node.members) {
2817+
// If the case pattern and body don't contain any splits, then we allow
2818+
// the entire case on one line:
2819+
//
2820+
// switch (obj) {
2821+
// case 1:
2822+
// case 2: print('one or two');
2823+
// default: print('other');
2824+
// }
2825+
//
2826+
// We wrap the rule for this around the entire case so that a split in
2827+
// the pattern or the case statement forces splitting after the ":" too.
2828+
builder.startLazyRule();
27832829

2784-
builder.indent();
2785-
builder.startBlockArgumentNesting();
2786-
builder.nestExpression();
2787-
visit(node.guardedPattern.pattern);
2788-
builder.unnest();
2789-
builder.endBlockArgumentNesting();
2790-
builder.unindent();
2830+
_visitLabels(member.labels);
2831+
token(member.keyword);
27912832

2792-
visit(node.guardedPattern.whenClause);
2793-
token(node.colon);
2833+
if (member is SwitchCase) {
2834+
space();
2835+
visit(member.expression);
2836+
} else if (member is SwitchPatternCase) {
2837+
space();
27942838

2795-
builder.indent();
2796-
newline();
2839+
var whenClause = member.guardedPattern.whenClause;
2840+
if (whenClause == null) {
2841+
builder.indent();
2842+
visit(member.guardedPattern.pattern);
2843+
builder.unindent();
2844+
} else {
2845+
// Wrap the when clause rule around the pattern so that if the pattern
2846+
// splits then we split before "when" too.
2847+
builder.startRule();
2848+
builder.nestExpression();
2849+
builder.startBlockArgumentNesting();
2850+
visit(member.guardedPattern.pattern);
2851+
split();
2852+
_visitWhenClause(whenClause);
2853+
builder.endBlockArgumentNesting();
2854+
builder.unnest();
2855+
builder.endRule();
2856+
}
2857+
}
27972858

2798-
visitNodes(node.statements, between: oneOrTwoNewlines);
2799-
builder.unindent();
2859+
token(member.colon);
2860+
2861+
if (member.statements.isNotEmpty) {
2862+
builder.indent();
2863+
split();
2864+
visitNodes(member.statements, between: oneOrTwoNewlines);
2865+
builder.unindent();
2866+
oneOrTwoNewlines();
2867+
} else {
2868+
// Don't preserve blank lines between empty cases.
2869+
newline();
2870+
}
2871+
2872+
builder.endRule();
2873+
}
2874+
2875+
newline();
2876+
_endBody(node.rightBracket, forceSplit: true);
28002877
}
28012878

2802-
@override
2803-
void visitSwitchStatement(SwitchStatement node) {
2879+
/// Visits the `switch (expr)` part of a switch statement or expression.
2880+
void _visitSwitchValue(Token switchKeyword, Token leftParenthesis,
2881+
Expression value, Token rightParenthesis) {
28042882
builder.nestExpression();
2805-
token(node.switchKeyword);
2883+
token(switchKeyword);
28062884
space();
2807-
token(node.leftParenthesis);
2885+
token(leftParenthesis);
28082886
soloZeroSplit();
2809-
visit(node.expression);
2810-
token(node.rightParenthesis);
2887+
visit(value);
2888+
token(rightParenthesis);
28112889
space();
28122890
builder.unnest();
2813-
2814-
_beginBody(node.leftBracket);
2815-
visitNodes(node.members, between: oneOrTwoNewlines, after: newline);
2816-
_endBody(node.rightBracket, forceSplit: true);
28172891
}
28182892

28192893
@override
@@ -2951,20 +3025,6 @@ class SourceVisitor extends ThrowingAstVisitor {
29513025
});
29523026
}
29533027

2954-
@override
2955-
void visitWhenClause(WhenClause node) {
2956-
builder.startRule();
2957-
split();
2958-
token(node.whenKeyword);
2959-
space();
2960-
builder.startBlockArgumentNesting();
2961-
builder.nestExpression();
2962-
visit(node.expression);
2963-
builder.unnest();
2964-
builder.endBlockArgumentNesting();
2965-
builder.endRule();
2966-
}
2967-
29683028
@override
29693029
void visitWhileStatement(WhileStatement node) {
29703030
builder.nestExpression();
@@ -3644,19 +3704,20 @@ class SourceVisitor extends ThrowingAstVisitor {
36443704
space();
36453705
token(leftParenthesis);
36463706

3647-
if (caseClause != null) {
3707+
if (caseClause == null) {
3708+
// Simple if with no "case".
3709+
visit(condition);
3710+
} else {
3711+
// If-case.
3712+
36483713
// Wrap the rule for splitting before "case" around the value expression
36493714
// so that if the value splits, we split before "case" too.
3650-
builder.startRule();
3715+
var caseRule = Rule();
3716+
builder.startRule(caseRule);
36513717

3652-
// Nest the condition so that it indents deeper than the case clause.
3653-
builder.nestExpression();
3654-
}
3655-
3656-
visit(condition);
3718+
visit(condition);
36573719

3658-
// If-case clause.
3659-
if (caseClause != null) {
3720+
// "case" and pattern.
36603721
split();
36613722
token(caseClause.caseKeyword);
36623723
space();
@@ -3665,15 +3726,34 @@ class SourceVisitor extends ThrowingAstVisitor {
36653726
visit(caseClause.guardedPattern.pattern);
36663727
builder.unnest();
36673728
builder.endBlockArgumentNesting();
3668-
builder.endRule();
3669-
visit(caseClause.guardedPattern.whenClause);
3729+
3730+
builder.endRule(); // Case rule.
3731+
3732+
var whenClause = caseClause.guardedPattern.whenClause;
3733+
if (whenClause != null) {
3734+
// Wrap the rule for "when" around the guard so that a split in the
3735+
// guard splits at "when" too.
3736+
builder.startRule();
3737+
split();
3738+
builder.startBlockArgumentNesting();
3739+
builder.nestExpression();
3740+
_visitWhenClause(whenClause);
3741+
builder.unnest();
3742+
builder.endBlockArgumentNesting();
3743+
builder.endRule(); // Guard rule.
3744+
}
36703745
}
36713746

36723747
token(rightParenthesis);
3673-
if (caseClause != null) builder.unnest();
36743748
builder.unnest();
36753749
}
36763750

3751+
void _visitWhenClause(WhenClause whenClause) {
3752+
token(whenClause.whenKeyword);
3753+
space();
3754+
visit(whenClause.expression);
3755+
}
3756+
36773757
/// Writes the separator between a type annotation and a variable or
36783758
/// parameter. If the preceding type annotation ends in a delimited list of
36793759
/// elements that have block formatting, then we don't split between the

test/comments/statements.stmt

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -75,41 +75,4 @@ main() {
7575
<<<
7676
main() {
7777
while (b) /*unreachable*/ {}
78-
}
79-
>>> blank lines before comments in switch
80-
main() {
81-
switch (n) {
82-
83-
84-
// comment
85-
case 0:
86-
87-
88-
89-
// comment
90-
91-
92-
93-
case 1:
94-
;
95-
96-
97-
// comment
98-
99-
100-
}
101-
}
102-
<<<
103-
main() {
104-
switch (n) {
105-
// comment
106-
case 0:
107-
108-
// comment
109-
110-
case 1:
111-
;
112-
113-
// comment
114-
}
11578
}

0 commit comments

Comments
 (0)