Skip to content

Commit 210b073

Browse files
committed
Change the block format heuristics.
This produces output that better matches what users expected in the associated issues. I think it also looks better in the affected regression tests. And I ran this on a large corpus and looking at the diffs, these rules seem to be much better across the board. The new rules are basically: * A block formatted argument list can't end up with named arguments on multiple lines. * Only one trailing argument is allowed after the block argument. The existing rules are also kept: * Prefer a function block argument over a collection one. * Only allow a single block argument. * Allow the first argument to be adjacent strings with a later function block argument (to handle stuff like test() and group()). Fix #1527. Fix #1528. Fix #1543.
1 parent cb61086 commit 210b073

21 files changed

+566
-241
lines changed

lib/src/ast_extensions.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ import 'package:analyzer/dart/ast/token.dart';
77
import 'piece/list.dart';
88

99
extension AstNodeExtensions on AstNode {
10+
/// When this node is in an argument list, what kind of block formatting
11+
/// category it belongs to.
12+
BlockFormat get blockFormatType => switch (this) {
13+
AdjacentStrings(indentStrings: true) =>
14+
BlockFormat.indentedAdjacentStrings,
15+
AdjacentStrings() => BlockFormat.unindentedAdjacentStrings,
16+
Expression(:var blockFormatType) => blockFormatType,
17+
_ => BlockFormat.none,
18+
};
19+
1020
/// The first token at the beginning of this AST node, not including any
1121
/// tokens for leading doc comments.
1222
///

lib/src/front_end/delimited_list_builder.dart

Lines changed: 92 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -441,97 +441,111 @@ class DelimitedListBuilder {
441441
/// Stores the result of this calculation by setting flags on the
442442
/// [ListElement]s.
443443
void _setBlockArgument(List<AstNode> arguments) {
444-
var firstIsUnindentedAdjacentStrings = false;
445-
var functions = <int>[];
446-
var collections = <int>[];
447-
var adjacentStrings = <int>[];
448-
449-
for (var i = 0; i < arguments.length; i++) {
450-
var argument = arguments[i];
451-
// See if it's an expression that supports block formatting.
452-
var format = switch (argument) {
453-
AdjacentStrings(indentStrings: true) =>
454-
BlockFormat.indentedAdjacentStrings,
455-
AdjacentStrings() => BlockFormat.unindentedAdjacentStrings,
456-
Expression() => argument.blockFormatType,
457-
DartPattern() when argument.canBlockSplit => BlockFormat.collection,
458-
_ => BlockFormat.none,
459-
};
460-
461-
if (i == 0 && format == BlockFormat.unindentedAdjacentStrings) {
462-
firstIsUnindentedAdjacentStrings = true;
444+
var candidateIndex = _candidateBlockArgument(arguments);
445+
if (candidateIndex == -1) return;
446+
447+
// Only allow up to one trailing argument after the block argument. This
448+
// handles the common `tags` and `timeout` named arguments in `test()` and
449+
// `group()` while still mostly having the block argument be at the end of
450+
// the argument list.
451+
if (candidateIndex < arguments.length - 2) return;
452+
453+
// If there are multiple named arguments, they should never end up on
454+
// separate lines (unless the whole argument list fully splits). Otherwise,
455+
// it's too easy for an argument name to get buried in the middle of a line.
456+
// So we look for named arguments before, on, and after the candidate
457+
// argument. If more than one of those sections of arguments has a named
458+
// argument, then we don't allow the block argument.
459+
var namedSections = 0;
460+
bool hasNamedArgument(int from, int to) {
461+
for (var i = from; i < to; i++) {
462+
if (arguments[i] is NamedExpression) return true;
463463
}
464464

465-
switch (format) {
466-
case BlockFormat.function:
467-
functions.add(i);
468-
case BlockFormat.collection:
469-
collections.add(i);
470-
case BlockFormat.invocation:
471-
// We don't allow function calls as block elements partially for style
472-
// and partially for performance. It often doesn't look great to let
473-
// nested function calls pack arbitrarily deeply as block arguments:
474-
//
475-
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(
476-
// content: Text(
477-
// localizations.demoSnackbarsAction,
478-
// )));
479-
//
480-
// This is better when expanded like:
481-
//
482-
// ScaffoldMessenger.of(context).showSnackBar(
483-
// SnackBar(
484-
// content: Text(
485-
// localizations.demoSnackbarsAction,
486-
// ),
487-
// ),
488-
// );
489-
//
490-
// Also, when invocations can be block arguments, which themselves
491-
// may contain block arguments, it's easy to run into combinatorial
492-
// performance in the solver as it tries to determine which of the
493-
// nested calls should and shouldn't be block formatted.
494-
break;
495-
case BlockFormat.indentedAdjacentStrings:
496-
case BlockFormat.unindentedAdjacentStrings:
497-
adjacentStrings.add(i);
498-
case BlockFormat.none:
499-
break; // Not a block element.
500-
}
465+
return false;
501466
}
502467

503-
// TODO(tall): These heuristics will probably need some iteration.
504-
switch ((functions, collections, adjacentStrings)) {
505-
// Only allow block formatting in an argument list containing adjacent
506-
// strings when:
507-
//
508-
// 1. The block argument is a function expression.
509-
// 2. It is the second argument, following an adjacent strings expression.
510-
// 3. There are no other adjacent strings in the argument list.
511-
//
512-
// This matches the `test()` and `group()` and other similar APIs where
513-
// you have a message string followed by a block-like function expression
514-
// but little else.
515-
// TODO(tall): We may want to iterate on these heuristics. For now,
516-
// starting with something very narrowly targeted.
517-
case ([1], _, [0]):
468+
if (hasNamedArgument(0, candidateIndex)) namedSections++;
469+
if (hasNamedArgument(candidateIndex, candidateIndex + 1)) namedSections++;
470+
if (hasNamedArgument(candidateIndex + 1, arguments.length)) namedSections++;
471+
472+
if (namedSections > 1) return;
473+
474+
// Edge case: If the first argument is adjacent strings and the second
475+
// argument is a function literal, with optionally a third non-block
476+
// argument, then treat the function as the block argument.
477+
//
478+
// This matches the `test()` and `group()` and other similar APIs where
479+
// you have a message string followed by a block-like function expression
480+
// but little else, as in:
481+
//
482+
// test('Some long test description '
483+
// 'that splits into multiple lines.', () {
484+
// expect(1 + 2, 3);
485+
// });
486+
if (candidateIndex == 1 &&
487+
arguments[0] is! NamedExpression &&
488+
arguments[1] is! NamedExpression) {
489+
if ((arguments[0].blockFormatType, arguments[1].blockFormatType)
490+
case (
491+
BlockFormat.unindentedAdjacentStrings ||
492+
BlockFormat.indentedAdjacentStrings,
493+
BlockFormat.function
494+
)) {
518495
// The adjacent strings.
519496
_elements[0].allowNewlinesWhenUnsplit = true;
520-
if (firstIsUnindentedAdjacentStrings) {
497+
if (arguments[0].blockFormatType ==
498+
BlockFormat.unindentedAdjacentStrings) {
521499
_elements[0].indentWhenBlockFormatted = true;
522500
}
523501

524502
// The block-formattable function.
525503
_elements[1].allowNewlinesWhenUnsplit = true;
504+
return;
505+
}
506+
}
526507

527-
// A function expression takes precedence over other block arguments.
528-
case ([var element], _, _):
529-
_elements[element].allowNewlinesWhenUnsplit = true;
508+
// If we get here, we have a block argument.
509+
_elements[candidateIndex].allowNewlinesWhenUnsplit = true;
510+
}
511+
512+
/// If an argument in [arguments] is a candidate to be block formatted,
513+
/// returns its index.
514+
///
515+
/// Otherwise, returns `-1`.
516+
int _candidateBlockArgument(List<AstNode> arguments) {
517+
var functionIndex = -1;
518+
var collectionIndex = -1;
519+
// var stringIndex = -1;
520+
521+
for (var i = 0; i < arguments.length; i++) {
522+
// See if it's an expression that supports block formatting.
523+
switch (arguments[i].blockFormatType) {
524+
case BlockFormat.function:
525+
if (functionIndex >= 0) {
526+
functionIndex = -2;
527+
} else {
528+
functionIndex = i;
529+
}
530+
531+
case BlockFormat.collection:
532+
if (collectionIndex >= 0) {
533+
collectionIndex = -2;
534+
} else {
535+
collectionIndex = i;
536+
}
530537

531-
// A single collection literal can be block formatted even if there are
532-
// other arguments.
533-
case ([], [var element], _):
534-
_elements[element].allowNewlinesWhenUnsplit = true;
538+
case BlockFormat.invocation:
539+
case BlockFormat.indentedAdjacentStrings:
540+
case BlockFormat.unindentedAdjacentStrings:
541+
case BlockFormat.none:
542+
break; // Normal argument.
543+
}
535544
}
545+
546+
if (functionIndex >= 0) return functionIndex;
547+
if (collectionIndex >= 0) return collectionIndex;
548+
549+
return -1;
536550
}
537551
}

lib/src/piece/list.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,9 +398,10 @@ enum BlockFormat {
398398
/// elements.
399399
function,
400400

401-
/// The element is a collection literal.
401+
/// The element is a collection literal or multiline string literal.
402402
///
403-
/// These can be block formatted even when there are other arguments.
403+
/// If there is only one of these and no [BlockFormat.function] elements, then
404+
/// it can be block formatted.
404405
collection,
405406

406407
/// A function or method invocation.
@@ -410,7 +411,7 @@ enum BlockFormat {
410411

411412
/// The element is an adjacent strings expression that's in an list that
412413
/// requires its subsequent lines to be indented (because there are other
413-
/// string literal in the list).
414+
/// string literals in the list).
414415
indentedAdjacentStrings,
415416

416417
/// The element is an adjacent strings expression that's in an list that
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
40 columns |
2+
### Test how block formatting functions like test(), group(), that start with
3+
### leading adjacent strings behaves.
4+
>>> Adjacent strings preceding a function expression doesn't prevent block formatting.
5+
test('First adjacent string' 'second adjacent string'
6+
'third adjacent string', () async {
7+
;
8+
});
9+
<<<
10+
test('First adjacent string'
11+
'second adjacent string'
12+
'third adjacent string', () async {
13+
;
14+
});
15+
>>> Don't block format a function with a preceding adjacent string if it doesn't fit.
16+
test('First adjacent string' 'second long adjacent string', () async {
17+
;
18+
});
19+
<<<
20+
test(
21+
'First adjacent string'
22+
'second long adjacent string',
23+
() async {
24+
;
25+
},
26+
);
27+
>>> Don't block format adjacent strings preceding a non-function block argument.
28+
test('First adjacent string'
29+
'second adjacent string'
30+
'third adjacent string', [
31+
element1,
32+
element2,
33+
element3,
34+
element4,
35+
]);
36+
<<<
37+
test(
38+
'First adjacent string'
39+
'second adjacent string'
40+
'third adjacent string',
41+
[
42+
element1,
43+
element2,
44+
element3,
45+
element4,
46+
],
47+
);
48+
>>> Another string argument doesn't prevent block formatting.
49+
test('First string line 1' 'first string line 2', () {
50+
;
51+
}, 'Another simple string');
52+
<<<
53+
test('First string line 1'
54+
'first string line 2', () {
55+
;
56+
}, 'Another simple string');
57+
>>> Multiple trailing arguments prevent block formatting.
58+
test('First string line 1' 'first string line 2', () {
59+
;
60+
}, trailing, another);
61+
<<<
62+
test(
63+
'First string line 1'
64+
'first string line 2',
65+
() {
66+
;
67+
},
68+
trailing,
69+
another,
70+
);
71+
>>> Don't block format if the leading string is named.
72+
test(named: 'First string line 1' 'first string line 2', () {
73+
;
74+
});
75+
<<<
76+
test(
77+
named:
78+
'First string line 1'
79+
'first string line 2',
80+
() {
81+
;
82+
},
83+
);
84+
>>> Don't block format if the leading function is named.
85+
test('First string line 1' 'first string line 2', named: () {
86+
;
87+
});
88+
<<<
89+
test(
90+
'First string line 1'
91+
'first string line 2',
92+
named: () {
93+
;
94+
},
95+
);
96+
>>> Allow block formatting when the trailing argument is named.
97+
test('First string line 1' 'first string line 2', () {
98+
;
99+
}, named: arg);
100+
<<<
101+
test('First string line 1'
102+
'first string line 2', () {
103+
;
104+
}, named: arg);

0 commit comments

Comments
 (0)