Skip to content

Commit 05d08d9

Browse files
authored
Tweak block argument heuristics (#1548)
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 494a1d7 commit 05d08d9

22 files changed

+604
-267
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: 122 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,6 @@ class DelimitedListBuilder {
6262
});
6363
}
6464

65-
if (_style.allowBlockElement) _setBlockElementFormatting();
66-
6765
var piece =
6866
ListPiece(_leftBracket, _elements, _blanksAfter, _rightBracket, _style);
6967
if (_mustSplit || forceSplit) piece.pin(State.split);
@@ -134,8 +132,8 @@ class DelimitedListBuilder {
134132
/// [addCommentsBefore()] for the first token in the [piece].
135133
///
136134
/// Assumes there is no comma after this piece.
137-
void add(Piece piece, [BlockFormat format = BlockFormat.none]) {
138-
_elements.add(ListElementPiece(_leadingComments, piece, format));
135+
void add(Piece piece) {
136+
_elements.add(ListElementPiece(_leadingComments, piece));
139137
_leadingComments.clear();
140138
_commentsBeforeComma = CommentSequence.empty;
141139
}
@@ -152,25 +150,33 @@ class DelimitedListBuilder {
152150
// Handle comments between the preceding element and this one.
153151
addCommentsBefore(element.firstNonCommentToken);
154152

155-
// See if it's an expression that supports block formatting.
156-
var format = switch (element) {
157-
AdjacentStrings(indentStrings: true) =>
158-
BlockFormat.indentedAdjacentStrings,
159-
AdjacentStrings() => BlockFormat.unindentedAdjacentStrings,
160-
Expression() => element.blockFormatType,
161-
DartPattern() when element.canBlockSplit => BlockFormat.collection,
162-
_ => BlockFormat.none,
163-
};
164-
165153
// Traverse the element itself.
166-
add(_visitor.nodePiece(element), format);
154+
add(_visitor.nodePiece(element));
167155

168156
var nextToken = element.endToken.next!;
169157
if (nextToken.lexeme == ',') {
170158
_commentsBeforeComma = _visitor.comments.takeCommentsBefore(nextToken);
171159
}
172160
}
173161

162+
/// Visits a list of [elements].
163+
///
164+
/// If [allowBlockArgument] is `true`, then allows one element to receive
165+
/// block formatting if appropriate, as in:
166+
///
167+
/// function(argument, [
168+
/// block,
169+
/// like,
170+
/// ], argument);
171+
void visitAll(List<AstNode> elements, {bool allowBlockArgument = false}) {
172+
for (var i = 0; i < elements.length; i++) {
173+
var element = elements[i];
174+
visit(element);
175+
}
176+
177+
if (allowBlockArgument) _setBlockArgument(elements);
178+
}
179+
174180
/// Inserts an inner left delimiter between two elements.
175181
///
176182
/// This is used for parameter lists when there are both mandatory and
@@ -398,6 +404,14 @@ class DelimitedListBuilder {
398404
);
399405
}
400406

407+
/// Given an argument list, determines which if any of the arguments should
408+
/// get special block-like formatting as in the list literal in:
409+
///
410+
/// function(argument, [
411+
/// block,
412+
/// like,
413+
/// ], argument);
414+
///
401415
/// Looks at the [BlockFormat] types of all of the elements to determine if
402416
/// one of them should be block formatted.
403417
///
@@ -426,85 +440,112 @@ class DelimitedListBuilder {
426440
///
427441
/// Stores the result of this calculation by setting flags on the
428442
/// [ListElement]s.
429-
void _setBlockElementFormatting() {
430-
// TODO(tall): These heuristics will probably need some iteration.
431-
var functions = <int>[];
432-
var collections = <int>[];
433-
var adjacentStrings = <int>[];
434-
435-
for (var i = 0; i < _elements.length; i++) {
436-
switch (_elements[i].blockFormat) {
437-
case BlockFormat.function:
438-
functions.add(i);
439-
case BlockFormat.collection:
440-
collections.add(i);
441-
case BlockFormat.invocation:
442-
// We don't allow function calls as block elements partially for style
443-
// and partially for performance. It often doesn't look great to let
444-
// nested function calls pack arbitrarily deeply as block arguments:
445-
//
446-
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(
447-
// content: Text(
448-
// localizations.demoSnackbarsAction,
449-
// )));
450-
//
451-
// This is better when expanded like:
452-
//
453-
// ScaffoldMessenger.of(context).showSnackBar(
454-
// SnackBar(
455-
// content: Text(
456-
// localizations.demoSnackbarsAction,
457-
// ),
458-
// ),
459-
// );
460-
//
461-
// Also, when invocations can be block arguments, which themselves
462-
// may contain block arguments, it's easy to run into combinatorial
463-
// performance in the solver as it tries to determine which of the
464-
// nested calls should and shouldn't be block formatted.
465-
break;
466-
case BlockFormat.indentedAdjacentStrings:
467-
case BlockFormat.unindentedAdjacentStrings:
468-
adjacentStrings.add(i);
469-
case BlockFormat.none:
470-
break; // Not a block element.
443+
void _setBlockArgument(List<AstNode> arguments) {
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;
471463
}
464+
465+
return false;
472466
}
473467

474-
switch ((functions, collections, adjacentStrings)) {
475-
// Only allow block formatting in an argument list containing adjacent
476-
// strings when:
477-
//
478-
// 1. The block argument is a function expression.
479-
// 2. It is the second argument, following an adjacent strings expression.
480-
// 3. There are no other adjacent strings in the argument list.
481-
//
482-
// This matches the `test()` and `group()` and other similar APIs where
483-
// you have a message string followed by a block-like function expression
484-
// but little else.
485-
// TODO(tall): We may want to iterate on these heuristics. For now,
486-
// starting with something very narrowly targeted.
487-
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+
)) {
488495
// The adjacent strings.
489496
_elements[0].allowNewlinesWhenUnsplit = true;
490-
if (_elements[0].blockFormat == BlockFormat.unindentedAdjacentStrings) {
497+
if (arguments[0].blockFormatType ==
498+
BlockFormat.unindentedAdjacentStrings) {
491499
_elements[0].indentWhenBlockFormatted = true;
492500
}
493501

494502
// The block-formattable function.
495503
_elements[1].allowNewlinesWhenUnsplit = true;
504+
return;
505+
}
506+
}
507+
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+
}
496530

497-
// A function expression takes precedence over other block arguments.
498-
case ([var element], _, _):
499-
_elements[element].allowNewlinesWhenUnsplit = true;
531+
case BlockFormat.collection:
532+
if (collectionIndex >= 0) {
533+
collectionIndex = -2;
534+
} else {
535+
collectionIndex = i;
536+
}
500537

501-
// A single collection literal can be block formatted even if there are
502-
// other arguments.
503-
case ([], [var element], _):
504-
_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+
}
505544
}
506545

507-
// If we get here, there are no block element, or it's ambiguous as to
508-
// which one should be it so none are.
546+
if (functionIndex >= 0) return functionIndex;
547+
if (collectionIndex >= 0) return collectionIndex;
548+
549+
return -1;
509550
}
510551
}

lib/src/front_end/piece_factory.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ mixin PieceFactory {
123123
leftBracket: leftBracket,
124124
elements,
125125
rightBracket: rightBracket,
126-
style: const ListStyle(allowBlockElement: true));
126+
allowBlockArgument: true);
127127
}
128128

129129
/// Writes a bracket-delimited block or declaration body.
@@ -972,7 +972,8 @@ mixin PieceFactory {
972972
{required Token leftBracket,
973973
required Token rightBracket,
974974
ListStyle style = const ListStyle(),
975-
bool preserveNewlines = false}) {
975+
bool preserveNewlines = false,
976+
bool allowBlockArgument = false}) {
976977
// If the list is completely empty, write the brackets directly inline so
977978
// that we create fewer pieces.
978979
if (!elements.canSplit(rightBracket)) {
@@ -988,7 +989,7 @@ mixin PieceFactory {
988989
if (preserveNewlines && elements.containsLineComments(rightBracket)) {
989990
_preserveNewlinesInCollection(elements, builder);
990991
} else {
991-
elements.forEach(builder.visit);
992+
builder.visitAll(elements, allowBlockArgument: allowBlockArgument);
992993
}
993994

994995
builder.rightBracket(rightBracket);

lib/src/piece/list.dart

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,6 @@ final class ListElementPiece extends Piece {
252252

253253
final Piece? _content;
254254

255-
/// What kind of block formatting can be applied to this element.
256-
final BlockFormat blockFormat;
257-
258255
/// Whether newlines are allowed in this element when this list is unsplit.
259256
///
260257
/// This is generally only true for a single "block" element, as in:
@@ -312,16 +309,13 @@ final class ListElementPiece extends Piece {
312309
/// delimiter (here `,` and 2).
313310
int _commentsBeforeDelimiter = 0;
314311

315-
ListElementPiece(
316-
List<Piece> leadingComments, Piece element, BlockFormat format)
312+
ListElementPiece(List<Piece> leadingComments, Piece element)
317313
: _leadingComments = [...leadingComments],
318-
_content = element,
319-
blockFormat = format;
314+
_content = element;
320315

321316
ListElementPiece.comment(Piece comment)
322317
: _leadingComments = const [],
323-
_content = null,
324-
blockFormat = BlockFormat.none {
318+
_content = null {
325319
_hangingComments.add(comment);
326320
}
327321

@@ -404,9 +398,10 @@ enum BlockFormat {
404398
/// elements.
405399
function,
406400

407-
/// The element is a collection literal.
401+
/// The element is a collection literal or multiline string literal.
408402
///
409-
/// 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.
410405
collection,
411406

412407
/// A function or method invocation.
@@ -416,7 +411,7 @@ enum BlockFormat {
416411

417412
/// The element is an adjacent strings expression that's in an list that
418413
/// requires its subsequent lines to be indented (because there are other
419-
/// string literal in the list).
414+
/// string literals in the list).
420415
indentedAdjacentStrings,
421416

422417
/// The element is an adjacent strings expression that's in an list that
@@ -459,18 +454,8 @@ class ListStyle {
459454
/// // ^ ^
460455
final bool spaceWhenUnsplit;
461456

462-
/// Whether an element in the list is allowed to have block-like formatting,
463-
/// as in:
464-
///
465-
/// function(argument, [
466-
/// block,
467-
/// like,
468-
/// ], argument);
469-
final bool allowBlockElement;
470-
471457
const ListStyle(
472458
{this.commas = Commas.trailing,
473459
this.splitCost = Cost.normal,
474-
this.spaceWhenUnsplit = false,
475-
this.allowBlockElement = false});
460+
this.spaceWhenUnsplit = false});
476461
}

0 commit comments

Comments
 (0)