Skip to content

Commit 7c4a960

Browse files
authored
Format || patterns like fallthrough cases in switch expressions. (#1620)
Switch statements allow multiple cases to share a body like: ```dart switch (obj) { case pattern1: case pattern2: body; } ``` Switch expressions don't support that, but `||` patterns are the idiomatic way to accomplish the same thing. Because of that, the formatter has some special formatting when the outermost pattern in a switch expression case is `||`: ```dart x = switch (obj) { pattern1 || pattern2 => body, }; ``` Note how the `pattern2` operand isn't indented. This PR extends that special handling to allow the `=>` on the same line as the `=>` even if the pattern is a split `||` pattern, like: ```dart x = switch (obj) { pattern1 || pattern2 => body, }; ``` And it prefers to split the `||` over the body when the body is block formatted: ```dart // Prefer: x = switch (obj) { pattern1 || pattern2 => function(argument), }; // Over: x = switch (obj) { pattern1 || pattern2 => function( argument, ), }; ``` This is one of those rules that's mostly a matter of taste, but I ran this on a large corpus and most of the diffs look better to me. Here are a few examples: ```dart // Before: typeName = switch (targetType) { DriftSqlType.int || DriftSqlType.bigInt || DriftSqlType.bool => 'INTEGER', DriftSqlType.string => 'CHAR', DriftSqlType.double => 'DOUBLE', DriftSqlType.blob => 'BINARY', DriftSqlType.dateTime => 'DATETIME', DriftSqlType.any => '', CustomSqlType() || DialectAwareSqlType() => targetType.sqlTypeName( context, ), }; // After: typeName = switch (targetType) { DriftSqlType.int || DriftSqlType.bigInt || DriftSqlType.bool => 'INTEGER', DriftSqlType.string => 'CHAR', DriftSqlType.double => 'DOUBLE', DriftSqlType.blob => 'BINARY', DriftSqlType.dateTime => 'DATETIME', DriftSqlType.any => '', CustomSqlType() || DialectAwareSqlType() => targetType.sqlTypeName(context), }; // Before: return switch (side) { AxisSide.right || AxisSide.left => titlesPadding.vertical + borderPadding.vertical, AxisSide.top || AxisSide.bottom => titlesPadding.horizontal + borderPadding.horizontal, }; // After: return switch (side) { AxisSide.right || AxisSide.left => titlesPadding.vertical + borderPadding.vertical, AxisSide.top || AxisSide.bottom => titlesPadding.horizontal + borderPadding.horizontal, }; // Before: final defaultConstraints = switch (side) { ShadSheetSide.top || ShadSheetSide.bottom => BoxConstraints( minWidth: mSize.width, ), ShadSheetSide.left || ShadSheetSide.right => BoxConstraints( minHeight: mSize.height, ), }; final defaultConstraints = switch (side) { ShadSheetSide.top || ShadSheetSide.bottom => BoxConstraints(minWidth: mSize.width), ShadSheetSide.left || ShadSheetSide.right => BoxConstraints(minHeight: mSize.height), }; ``` Fix #1602.
1 parent 47bcc07 commit 7c4a960

File tree

6 files changed

+69
-29
lines changed

6 files changed

+69
-29
lines changed

CHANGELOG.md

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

33
* Handle trailing commas in for-loop updaters (#1354).
4+
* Format `||` patterns like fallthrough cases in switch expressions (#1602).
45
* Handle comments and metadata before variables more gracefully (#1604).
56
* Ensure comment formatting is idempotent (#1606).
67
* Better indentation of leading comments on property accesses in binary operator

lib/src/piece/case.dart

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,20 @@ import 'piece.dart';
88

99
/// Piece for a case pattern, guard, and body in a switch expression.
1010
final class CaseExpressionPiece extends Piece {
11+
/// Split inside the body, which must be block formattable, like:
12+
///
13+
/// pattern => function(
14+
/// argument,
15+
/// ),
16+
static const State _blockSplitBody = State(1, cost: 0);
17+
1118
/// Split after the `=>` before the body.
12-
static const State _beforeBody = State(1);
19+
static const State _beforeBody = State(2);
1320

1421
/// Split before the `when` guard clause and after the `=>`.
15-
static const State _beforeWhenAndBody = State(2);
22+
static const State _beforeWhenAndBody = State(3);
1623

17-
/// The pattern the value is matched against along with the leading `case`.
24+
/// The pattern the value is matched against.
1825
final Piece _pattern;
1926

2027
/// If there is a `when` clause, that clause.
@@ -54,17 +61,23 @@ final class CaseExpressionPiece extends Piece {
5461

5562
@override
5663
List<State> get additionalStates => [
64+
if (_canBlockSplitBody) _blockSplitBody,
5765
_beforeBody,
5866
if (_guard != null) ...[_beforeWhenAndBody],
5967
];
6068

6169
@override
6270
bool allowNewlineInChild(State state, Piece child) {
6371
return switch (state) {
72+
// If the outermost pattern is `||`, then always let it split even while
73+
// allowing the body on the same line as `=>`.
74+
_ when child == _pattern && _patternIsLogicalOr => true,
75+
76+
// There are almost never splits in the arrow piece. It requires a comment
77+
// in a funny location, but if it happens, allow it.
6478
_ when child == _arrow => true,
65-
State.unsplit when child == _body && _canBlockSplitBody => true,
66-
_beforeBody when child == _pattern =>
67-
_guard == null || _patternIsLogicalOr,
79+
_blockSplitBody when child == _body => true,
80+
_beforeBody when child == _pattern => _guard == null,
6881
_beforeBody when child == _body => true,
6982
_beforeWhenAndBody => true,
7083
_ => false,
@@ -94,12 +107,13 @@ final class CaseExpressionPiece extends Piece {
94107
writer.space();
95108
writer.format(_arrow);
96109

97-
if (state != State.unsplit) writer.pushIndent(Indent.block);
110+
var indentBody = state != State.unsplit && state != _blockSplitBody;
111+
if (indentBody) writer.pushIndent(Indent.block);
98112

99113
writer.splitIf(state == _beforeBody || state == _beforeWhenAndBody);
100114
writer.format(_body);
101115

102-
if (state != State.unsplit) writer.popIndent();
116+
if (indentBody) writer.popIndent();
103117
}
104118

105119
@override

test/tall/expression/switch.stmt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,7 @@ e = switch (obj) {
120120
e = switch (obj) {
121121
oneConstant ||
122122
twoConstant ||
123-
threeConstant =>
124-
body,
123+
threeConstant => body,
125124
};
126125
>>> Nested logic-or operands are indented.
127126
e = switch (obj) {
@@ -175,4 +174,15 @@ e = switch (obj) {
175174
veryLongElement,
176175
veryLongElement,
177176
),
178-
};
177+
};
178+
>>> Prefer to split `||` pattern instead of case body.
179+
e = switch (obj) {
180+
pattern || another => function(
181+
argument,
182+
),
183+
};
184+
<<<
185+
e = switch (obj) {
186+
pattern ||
187+
another => function(argument),
188+
};

test/tall/expression/switch_guard.stmt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,7 @@ e = switch (obj) {
9696
<<<
9797
e = switch (obj) {
9898
veryVeryLongPattern ||
99-
reallyMustSplitHere when true =>
100-
body,
99+
reallyMustSplitHere when true => body,
101100
};
102101
>>> Outermost logic-or split in pattern, expression split in guard.
103102
e = switch (obj) {

test/tall/regression/1100/1197.unit

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ main() {
2525
}
2626
}
2727
<<<
28+
### TODO(1466): Ideally, the first case would also split at the `||` instead of
29+
### of before `.`, but the formatter can't distinguish that case without fixing
30+
### #1466.
2831
main() {
2932
{
3033
return TextFieldTapRegion(
@@ -38,14 +41,13 @@ main() {
3841
TargetPlatform.android ||
3942
TargetPlatform.fuchsia ||
4043
TargetPlatform.linux ||
41-
TargetPlatform.windows =>
42-
_renderEditable.selectWordsInRange(
43-
from:
44-
longPressMoveUpdateDetails.globalPosition -
45-
longPressMoveUpdateDetails.offsetFromOrigin,
46-
to: longPressMoveUpdateDetails.globalPosition,
47-
cause: SelectionChangedCause.longPress,
48-
),
44+
TargetPlatform.windows => _renderEditable.selectWordsInRange(
45+
from:
46+
longPressMoveUpdateDetails.globalPosition -
47+
longPressMoveUpdateDetails.offsetFromOrigin,
48+
to: longPressMoveUpdateDetails.globalPosition,
49+
cause: SelectionChangedCause.longPress,
50+
),
4951
});
5052
},
5153
);
@@ -77,6 +79,9 @@ main() {
7779
}
7880
}
7981
<<<
82+
### TODO(1466): Ideally, the first case would also split at the `||` instead of
83+
### of before `.`, but the formatter can't distinguish that case without fixing
84+
### #1466.
8085
main() {
8186
{
8287
return TextFieldTapRegion(
@@ -92,14 +97,13 @@ main() {
9297
TargetPlatform.android ||
9398
TargetPlatform.fuchsia ||
9499
TargetPlatform.linux ||
95-
TargetPlatform.windows =>
96-
_renderEditable.selectWordsInRange(
97-
from:
98-
longPressMoveUpdateDetails.globalPosition -
99-
longPressMoveUpdateDetails.offsetFromOrigin,
100-
to: longPressMoveUpdateDetails.globalPosition,
101-
cause: SelectionChangedCause.longPress,
102-
),
100+
TargetPlatform.windows => _renderEditable.selectWordsInRange(
101+
from:
102+
longPressMoveUpdateDetails.globalPosition -
103+
longPressMoveUpdateDetails.offsetFromOrigin,
104+
to: longPressMoveUpdateDetails.globalPosition,
105+
cause: SelectionChangedCause.longPress,
106+
),
103107
},
104108
);
105109
}

test/tall/regression/1600/1602.stmt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
>>> (indent 4)
2+
return switch (_advance().type) {
3+
TokenType.float || TokenType.int || TokenType.string => LiteralExpression(
4+
_previous.value!,
5+
),
6+
};
7+
<<<
8+
return switch (_advance().type) {
9+
TokenType.float ||
10+
TokenType.int ||
11+
TokenType.string => LiteralExpression(_previous.value!),
12+
};

0 commit comments

Comments
 (0)