Skip to content

Commit 02d5bc2

Browse files
authored
When the RHS of => is a function call, prefer to split at the =>. (#1523)
We use the same AssignPiece rule for `=`, `=>`, and `:`. And that rule uniformly handles all kinds of block-formattable right hand sides: collection literals, switch expressions, and function calls. For the most part, that works well and provides nice consistent formatting. Users generally like: ```dart // Better: variable = [ long, list, literal, ]; // Worse: variable = [long, list, literal]; ``` And: ```dart // Better: SomeWidget( children: [ long, list, literal, ], ); // Worse: SomeWidget( children: [long, list, literal], ); ``` And (with somewhat less consensus): ```dart // Better: variable = function( long, argument, list, ); // Worse: variable = function(long, argument, list); ``` Also, users have long requested and seem to like: ```dart // Better: class C { List<String> makeStuff() => [ long, list, literal, ]; } // Worse: class C { List<String> makeStuff() => [long, list, literal]; } ``` So based on all that, I just used uniform rules for all combinations of assignment constructs and delimited constructs. That means that this behavior falls out implicitly: ```dart // Better: class C { String doThing() => function( long, argument, list, ); } // Worse: class C { String doThing() => function(long, argument, list); } ``` But it turns out that that particular combination of `=>` with a function call on the right doesn't get the same user sentiment. Instead, most (including me) prefer: ```dart class C { String doThing() => function(long, argument, list); } ``` I think it's because this keeps the function name next to its arguments. With the other block-like forms: list literals, etc. there isn't really anything particularly interesting going on in the opening delimiter, so it makes sense to keep it on the same line as the `=>` since it's pretty much just punctuation. But a function call is a single coherent operation including the function and its arguments. So this PR tweaks the cost rule for AssignPiece. When the operator is `=>` and the RHS is a function call, we prefer to split at the `=>` if that lets the RHS stay unsplit.
1 parent 92b2fa8 commit 02d5bc2

File tree

15 files changed

+261
-268
lines changed

15 files changed

+261
-268
lines changed

lib/src/front_end/ast_node_visitor.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,9 @@ class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {
605605
nodePiece(node.expression, context: NodeContext.assignment);
606606

607607
pieces.add(AssignPiece(operatorPiece, expression,
608-
canBlockSplitRight: node.expression.canBlockSplit));
608+
canBlockSplitRight: node.expression.canBlockSplit,
609+
avoidBlockSplitRight:
610+
node.expression.blockFormatType == BlockFormat.invocation));
609611
pieces.token(node.semicolon);
610612
}
611613

lib/src/piece/assign.dart

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class AssignPiece extends Piece {
7373
static const State _blockSplitLeft = State(1);
7474

7575
/// Allow the right-hand side to block split.
76-
static const State _blockSplitRight = State(2);
76+
static const State _blockSplitRight = State(2, cost: 0);
7777

7878
/// Split at the operator.
7979
static const State _atOperator = State(3);
@@ -109,13 +109,34 @@ class AssignPiece extends Piece {
109109
/// ];
110110
final bool _canBlockSplitRight;
111111

112+
/// If `true` then prefer to split at the operator instead of block splitting
113+
/// the right side.
114+
///
115+
/// This is `true` for `=>` functions whose body is a function call. This
116+
/// keeps the called function next to its arguments instead having the
117+
/// function name stick to the `=>` while the arguments split. In other words,
118+
/// prefer:
119+
///
120+
/// someMethod() =>
121+
/// someFunction(argument, another);
122+
///
123+
/// Over:
124+
///
125+
/// someMethod() => someFunction(
126+
/// argument,
127+
/// another,
128+
/// );
129+
final bool _avoidBlockSplitRight;
130+
112131
AssignPiece(this._operator, this._right,
113132
{Piece? left,
114133
bool canBlockSplitLeft = false,
115-
bool canBlockSplitRight = false})
134+
bool canBlockSplitRight = false,
135+
bool avoidBlockSplitRight = false})
116136
: _left = left,
117137
_canBlockSplitLeft = canBlockSplitLeft,
118-
_canBlockSplitRight = canBlockSplitRight;
138+
_canBlockSplitRight = canBlockSplitRight,
139+
_avoidBlockSplitRight = avoidBlockSplitRight;
119140

120141
@override
121142
List<State> get additionalStates => [
@@ -126,6 +147,16 @@ class AssignPiece extends Piece {
126147
_atOperator,
127148
];
128149

150+
@override
151+
int stateCost(State state) {
152+
// Allow block splitting the right side, but increase the cost so that we
153+
// prefer splitting at the operator and not splitting in the right piece if
154+
// possible.
155+
if (state == _blockSplitRight && _avoidBlockSplitRight) return 1;
156+
157+
return super.stateCost(state);
158+
}
159+
129160
@override
130161
void applyConstraints(State state, Constrain constrain) {
131162
// Force the left side to block split when in that state.
@@ -140,63 +171,35 @@ class AssignPiece extends Piece {
140171
}
141172

142173
@override
143-
bool allowNewlineInChild(State state, Piece child) {
144-
if (state == State.unsplit) {
145-
if (child == _left) return false;
146-
147-
// Always allow block-splitting the right side if it supports it.
148-
if (child == _right) return _canBlockSplitRight;
149-
}
150-
151-
return true;
152-
}
174+
bool allowNewlineInChild(State state, Piece child) => state != State.unsplit;
153175

154176
@override
155177
void format(CodeWriter writer, State state) {
156-
switch (state) {
157-
case State.unsplit:
158-
_writeLeft(writer, allowNewlines: false);
159-
_writeOperator(writer);
160-
// Always allow block-splitting the right side if it supports it.
161-
_writeRight(writer, allowNewlines: _canBlockSplitRight);
162-
163-
case _atOperator:
164-
// When splitting at the operator, both operands may split or not and
165-
// will be indented if they do.
166-
writer.pushIndent(Indent.expression);
167-
_writeLeft(writer);
168-
_writeOperator(writer, split: state == _atOperator);
169-
_writeRight(writer);
170-
writer.popIndent();
171-
172-
case _blockSplitLeft:
173-
_writeLeft(writer);
174-
_writeOperator(writer);
175-
_writeRight(writer, indent: !_canBlockSplitRight);
176-
177-
case _blockSplitRight:
178-
_writeLeft(writer);
179-
_writeOperator(writer, split: state == _atOperator);
180-
_writeRight(writer);
181-
}
182-
}
178+
// When splitting at the operator, both operands may split or not and
179+
// will be indented if they do.
180+
if (state == _atOperator) writer.pushIndent(Indent.expression);
183181

184-
void _writeLeft(CodeWriter writer, {bool allowNewlines = true}) {
185182
if (_left case var left?) writer.format(left);
186-
}
187183

188-
void _writeOperator(CodeWriter writer, {bool split = false}) {
189184
writer.pushIndent(Indent.expression);
190185
writer.format(_operator);
191186
writer.popIndent();
192-
writer.splitIf(split);
193-
}
187+
writer.splitIf(state == _atOperator);
194188

195-
void _writeRight(CodeWriter writer,
196-
{bool indent = false, bool allowNewlines = true}) {
197-
if (indent) writer.pushIndent(Indent.expression);
189+
// If the left side block splits and the right doesn't, then indent the
190+
// right side if it splits as in:
191+
//
192+
// var [
193+
// a,
194+
// b,
195+
// ] = long +
196+
// expression;
197+
var indentRight = state == _blockSplitLeft && !_canBlockSplitRight;
198+
if (indentRight) writer.pushIndent(Indent.expression);
198199
writer.format(_right);
199-
if (indent) writer.popIndent();
200+
if (indentRight) writer.popIndent();
201+
202+
if (state == _atOperator) writer.popIndent();
200203
}
201204

202205
@override

test/tall/function/default_value.unit

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ f([
6666
f([callback(SomeType parameter) = const CallableClass(someLongConstantArgument, anotherConstantArgument)]) {}
6767
<<<
6868
f([
69-
callback(SomeType parameter) =
70-
const CallableClass(
71-
someLongConstantArgument,
72-
anotherConstantArgument,
73-
),
69+
callback(
70+
SomeType parameter,
71+
) = const CallableClass(
72+
someLongConstantArgument,
73+
anotherConstantArgument,
74+
),
7475
]) {}

test/tall/function/expression_body.unit

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,11 @@ function() => (
5252
longElement,
5353
longElement,
5454
);
55-
>>> Prefer block-like splitting for function calls.
55+
>>> Avoid block-like splitting for function calls.
5656
function() => another(argument, argument);
5757
<<<
58-
function() => another(
59-
argument,
60-
argument,
61-
);
58+
function() =>
59+
another(argument, argument);
6260
>>> Use block-like splitting for switch expressions.
6361
function() => switch (value) { 1 => 'one', 2 => 'two' };
6462
<<<

test/tall/regression/0100/0108.unit

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,8 @@ class ElementBinder {
276276
eventName,
277277
(_) => zone.run(
278278
() => bindAssignableProps.forEach(
279-
(propAndExp) => propAndExp[1].assign(
280-
scope.context,
281-
jsNode[propAndExp[0]],
282-
),
279+
(propAndExp) =>
280+
propAndExp[1].assign(scope.context, jsNode[propAndExp[0]]),
283281
),
284282
),
285283
),

test/tall/regression/0200/0217.stmt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ Annotation _getAnnotation(AnnotatedNode node, String name) => nodeMetadata
33
.firstWhere((annotation) => _isAnnotationType(annotation, name),
44
orElse: () => null);
55
<<<
6-
Annotation _getAnnotation(AnnotatedNode node, String name) => nodeMetadata
7-
.firstWhere(
6+
Annotation _getAnnotation(AnnotatedNode node, String name) =>
7+
nodeMetadata.firstWhere(
88
(annotation) => _isAnnotationType(annotation, name),
99
orElse: () => null,
1010
);
@@ -13,8 +13,8 @@ Annotation _getAnnotation(AnnotatedNode node, String name) => nodeMetadata
1313
.firstWhere((annotation) => _isAnnotationType(annotation, name),
1414
orElse: () => null);
1515
<<<
16-
Annotation _getAnnotation(AnnotatedNode node, String name) => nodeMetadata
17-
.firstWhere(
16+
Annotation _getAnnotation(AnnotatedNode node, String name) =>
17+
nodeMetadata.firstWhere(
1818
(annotation) => _isAnnotationType(annotation, name),
1919
orElse: () => null,
2020
);

0 commit comments

Comments
 (0)