Skip to content

Commit eaa3f2d

Browse files
authored
Make the eager argument list splitting heuristic more conservative. (#1700)
Some folks on the Flutter team pointed out that the previous rule is too aggressive and splits even simple common code like: ```dart Text('Item 1', style: TextStyle(color: Colors.white)); ``` That's probably simple enough to stay on one line. So this tweaks the heuristic to allow that to remain unsplit. Where the old heuristic split any argument list with a named argument that contained a nested named argument, this requires there to be at least *three* named arguments. It's sort of an arbitrary cutoff, but it seems to do a good job when run on a large corpus.
1 parent cd73f3e commit eaa3f2d

File tree

7 files changed

+255
-114
lines changed

7 files changed

+255
-114
lines changed

CHANGELOG.md

+13-12
Original file line numberDiff line numberDiff line change
@@ -151,26 +151,27 @@
151151
to read spread across multiple lines even if they would otherwise fit on a
152152
single line (#1660). The rules are basically:
153153

154-
* If an argument list contains any named arguments and contains any other
155-
calls whose argument lists contain any named arguments (directly or
156-
indirectly), then split the outer one. We make an exception where a named
157-
argument whose expression is a simple number, Boolean, or null literal
158-
doesn't count as a named argument.
154+
* If an argument list contains at least three named arguments, at least one
155+
of which must be directly in the argument list and at least one of which
156+
must be nested in an inner argument list, then force the outer one to split.
157+
We make an exception where a named argument whose expression is a simple
158+
number, Boolean, or null literal doesn't count as a named argument.
159159

160160
* If a list, set, or map literal is the immediate expression in a named
161-
argument and one of the above kinds of argument lists (directly or
162-
indirectly), then force the collection to split.
161+
argument and contains any argument lists with a named argument, then force
162+
the collection to split.
163163

164164
```dart
165165
// Before:
166-
Stack(children: [result, Semantics(label: indexLabel)]);
166+
TabBar(tabs: [Tab(text: 'A'), Tab(text: 'B')], labelColor: Colors.white70);
167167
168168
// After:
169-
Stack(
170-
children: [
171-
result,
172-
Semantics(label: indexLabel),
169+
TabBar(
170+
tabs: [
171+
Tab(text: 'A'),
172+
Tab(text: 'B'),
173173
],
174+
labelColor: Colors.white70,
174175
);
175176
```
176177

lib/src/front_end/expression_contents.dart

+55-31
Original file line numberDiff line numberDiff line change
@@ -51,40 +51,49 @@ class ExpressionContents {
5151
/// Begins tracking an argument list.
5252
void beginCall(List<AstNode> arguments) {
5353
var type = _Type.otherCall;
54-
var hasNontrivialNamedArgument = false;
54+
55+
// Count the non-trivial named arguments in this call.
56+
var namedArguments = 0;
5557
for (var argument in arguments) {
5658
if (argument is NamedExpression) {
5759
type = _Type.callWithNamedArgument;
58-
if (!_isTrivial(argument.expression)) {
59-
hasNontrivialNamedArgument = true;
60-
break;
61-
}
60+
if (!_isTrivial(argument.expression)) namedArguments++;
6261
}
6362
}
6463

65-
if (hasNontrivialNamedArgument) _stack.last.nontrivialCalls++;
66-
67-
_stack.add(_Contents(type));
64+
_stack.add(_Contents(type, namedArguments: namedArguments));
6865
}
6966

7067
/// Ends the most recently begun call and returns `true` if its argument list
7168
/// should eagerly split.
72-
bool endCall() {
69+
bool endCall(List<Expression> arguments) {
7370
var contents = _end();
7471

75-
// If this argument list has any named arguments and it contains another
76-
// call with any non-trivial named arguments, then split it. The idea here
77-
// is that when scanning a line of code, it's hard to tell which calls own
78-
// which named arguments. If there are named arguments at different levels
79-
// of nesting in an expression tree, it's better to split the outer one to
80-
// make that clearer.
72+
// If there are "too many" named arguments in this call and the calls it
73+
// contains, then split it.
74+
//
75+
// The basic idea is that when scanning a line of code, it's hard to tell
76+
// which calls own which named arguments if there are named arguments at
77+
// multiple levels in the call tree. Splitting makes that clearer. At the
78+
// same time, it's annoying it the formatter is too aggressive about
79+
// splitting an expression that feels simple enough to the reader to fit on
80+
// one line. (Especially because if the formatter does eagerly split it,
81+
// there's nothing they can do to *prevent* that.)
82+
//
83+
// The heuristic here tries to strike a "Goldilocks" balance between not
84+
// splitting too aggressively or too conservatively. The rule is that the
85+
// entire call tree must contain at least three named arguments, at least
86+
// one must be in the outermost call being split, and at least one must
87+
// *not* be in the outermost call.
88+
//
89+
// It would be simpler to split any call that has named arguments at
90+
// different nesting levels, but that's a little too aggressive and forces
91+
// common code like this to split:
8192
//
82-
// With this rule, any two named arguments in the middle of the same line
83-
// will always be owned by the same call. There may be a named argument at
84-
// the very beginning of the line owned by the surrounding call which is
85-
// forced to split.
86-
return (contents.type == _Type.callWithNamedArgument) &&
87-
contents.nontrivialCalls > 0;
93+
// Text('Item 1', style: TextStyle(color: Colors.white));
94+
return contents.totalNamedArguments > 2 &&
95+
contents.namedArguments > 0 &&
96+
contents.nestedNamedArguments > 0;
8897
}
8998

9099
/// Begin tracking a collection literal and its contents.
@@ -102,9 +111,9 @@ class ExpressionContents {
102111
if (contents.collections > 0) return true;
103112

104113
// If the collection is itself a named argument in a surrounding call that
105-
// is going to be forced to eagerly split, then split the collection too.
106-
// In this case, the collection is sort of like a vararg argument to the
107-
// call. Prefers:
114+
// may be be forced to eagerly split, then split the collection too. In
115+
// that case, the collection is sort of like a vararg argument to the call.
116+
// Prefers:
108117
//
109118
// TabBar(
110119
// tabs: <Widget>[
@@ -118,8 +127,14 @@ class ExpressionContents {
118127
// TabBar(
119128
// tabs: <Widget>[Tab(text: 'Tab 1'), Tab(text: 'Tab 2')],
120129
// );
121-
return contents.type == _Type.namedCollection &&
122-
contents.nontrivialCalls > 0;
130+
//
131+
// Splitting a collection is also helpful, because it shows each element
132+
// in parallel with each on its own line. But that's only true when there
133+
// are multiple elements, so we don't eagerly split collections with just a
134+
// single element.
135+
return elements.length > 1 &&
136+
contents.type == _Type.namedCollection &&
137+
contents.totalNamedArguments > 0;
123138
}
124139

125140
/// Ends the most recently begun operation and returns its contents.
@@ -129,7 +144,8 @@ class ExpressionContents {
129144
// Transitively include this operation's contents in the surrounding one.
130145
var parent = _stack.last;
131146
parent.collections += contents.collections;
132-
parent.nontrivialCalls += contents.nontrivialCalls;
147+
parent.nestedNamedArguments +=
148+
contents.namedArguments + contents.nestedNamedArguments;
133149

134150
return contents;
135151
}
@@ -169,11 +185,19 @@ class _Contents {
169185
/// this operation.
170186
int collections = 0;
171187

172-
/// The number of calls with non-trivial named arguments transitively inside
173-
/// this operation.
174-
int nontrivialCalls = 0;
188+
/// The number of non-trivial named arguments in this call's own argument
189+
/// list.
190+
int namedArguments = 0;
191+
192+
/// The number of non-trivial named arguments transitively inside this
193+
/// operation, but not including the call's own named arguments.
194+
int nestedNamedArguments = 0;
195+
196+
_Contents(this.type, {this.namedArguments = 0});
175197

176-
_Contents(this.type);
198+
/// The total number of non-trivial named arguments in this operation's own
199+
/// argument list and all of transitive contents.
200+
int get totalNamedArguments => namedArguments + nestedNamedArguments;
177201
}
178202

179203
enum _Type {

lib/src/front_end/piece_factory.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ mixin PieceFactory {
144144
);
145145

146146
// If the call is complex enough, force it to split even if it would fit.
147-
if (_contents.endCall()) {
147+
if (_contents.endCall(arguments)) {
148148
// Don't force an argument list to fully split if it could block split.
149149
// TODO(rnystrom): Ideally, if the argument list has a block argument, we
150150
// would force it to either block split or fully split, but disallow it

0 commit comments

Comments
 (0)