Skip to content

Commit 034118d

Browse files
authored
Format list, map, and record patterns. (#1170)
* Format list, map, and record patterns. Also test list, map, set, and record constants inside patterns. In the process of working on this, I noticed that the formatting for collection literal type arguments isn't ideal. In rare cases, it would split after the initial "<", which looks totally broken. I fixed that here too. I'd normally pull that out into a separate PR but it's mixed in with the other changes to _visitCollectionLiteral() so that would be hard. Also added some more tests around avoiding splitting between a named argument and a subsequent collection literal. The code was there to handle it but it wasn't tested well. (It's hard to come up with an example that hits that exact code path without formatting correctly for other reasons.) * Handle inferred field names in record patterns.
1 parent 8559543 commit 034118d

File tree

13 files changed

+878
-41
lines changed

13 files changed

+878
-41
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# 2.2.5-dev
22

3+
* Format patterns and related features.
4+
* Don't split after `<` in collection literals.
35
* Format record expressions and record type annotations.
46
* Better indentation of multiline function types inside type argument lists.
57
* Require `package:analyzer` `^5.1.0`.

lib/src/argument_list_visitor.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,8 @@ class ArgumentSublist {
476476
}
477477

478478
if (argument is NamedExpression) {
479-
visitor.visitNamedArgument(argument, rule as NamedRule);
479+
visitor.visitNamedNode(argument.name.label.token, argument.name.colon,
480+
argument.expression, rule as NamedRule);
480481
} else {
481482
visitor.visit(argument);
482483
}

lib/src/source_visitor.dart

Lines changed: 148 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ class SourceVisitor extends ThrowingAstVisitor {
255255
// and the ")" ends up on its own line.
256256
if (node.arguments.hasCommaAfter) {
257257
_visitCollectionLiteral(
258-
null, node.leftParenthesis, node.arguments, node.rightParenthesis);
258+
node.leftParenthesis, node.arguments, node.rightParenthesis);
259259
return;
260260
}
261261

@@ -289,7 +289,7 @@ class SourceVisitor extends ThrowingAstVisitor {
289289
// and the ")" ends up on its own line.
290290
if (arguments.hasCommaAfter) {
291291
_visitCollectionLiteral(
292-
null, node.leftParenthesis, arguments, node.rightParenthesis);
292+
node.leftParenthesis, arguments, node.rightParenthesis);
293293
return;
294294
}
295295

@@ -313,7 +313,7 @@ class SourceVisitor extends ThrowingAstVisitor {
313313
// and the ")" ends up on its own line.
314314
if (arguments.hasCommaAfter) {
315315
_visitCollectionLiteral(
316-
null, node.leftParenthesis, arguments, node.rightParenthesis);
316+
node.leftParenthesis, arguments, node.rightParenthesis);
317317
return;
318318
}
319319

@@ -2108,8 +2108,17 @@ class SourceVisitor extends ThrowingAstVisitor {
21082108
// Corner case: Splitting inside a list looks bad if there's only one
21092109
// element, so make those more costly.
21102110
var cost = node.elements.length <= 1 ? Cost.singleElementList : Cost.normal;
2111-
_visitCollectionLiteral(
2112-
node, node.leftBracket, node.elements, node.rightBracket, cost);
2111+
_visitCollectionLiteral(node.leftBracket, node.elements, node.rightBracket,
2112+
constKeyword: node.constKeyword,
2113+
typeArguments: node.typeArguments,
2114+
splitOuterCollection: true,
2115+
cost: cost);
2116+
}
2117+
2118+
@override
2119+
void visitListPattern(ListPattern node) {
2120+
_visitCollectionLiteral(node.leftBracket, node.elements, node.rightBracket,
2121+
typeArguments: node.typeArguments);
21132122
}
21142123

21152124
@override
@@ -2122,6 +2131,22 @@ class SourceVisitor extends ThrowingAstVisitor {
21222131
builder.unnest();
21232132
}
21242133

2134+
@override
2135+
void visitMapPattern(MapPattern node) {
2136+
_visitCollectionLiteral(node.leftBracket, node.elements, node.rightBracket,
2137+
typeArguments: node.typeArguments);
2138+
}
2139+
2140+
@override
2141+
void visitMapPatternEntry(MapPatternEntry node) {
2142+
builder.nestExpression();
2143+
visit(node.key);
2144+
token(node.separator);
2145+
soloSplit();
2146+
visit(node.value);
2147+
builder.unnest();
2148+
}
2149+
21252150
@override
21262151
void visitMethodDeclaration(MethodDeclaration node) {
21272152
_visitFunctionOrMethodDeclaration(
@@ -2232,7 +2257,7 @@ class SourceVisitor extends ThrowingAstVisitor {
22322257

22332258
@override
22342259
void visitNamedExpression(NamedExpression node) {
2235-
visitNamedArgument(node);
2260+
visitNamedNode(node.name.label.token, node.name.colon, node.expression);
22362261
}
22372262

22382263
@override
@@ -2373,7 +2398,34 @@ class SourceVisitor extends ThrowingAstVisitor {
23732398
void visitRecordLiteral(RecordLiteral node) {
23742399
modifier(node.constKeyword);
23752400
_visitCollectionLiteral(
2376-
node, node.leftParenthesis, node.fields, node.rightParenthesis);
2401+
node.leftParenthesis, node.fields, node.rightParenthesis,
2402+
isRecord: true);
2403+
}
2404+
2405+
@override
2406+
void visitRecordPattern(RecordPattern node) {
2407+
_visitCollectionLiteral(
2408+
node.leftParenthesis, node.fields, node.rightParenthesis,
2409+
isRecord: true);
2410+
}
2411+
2412+
@override
2413+
void visitRecordPatternField(RecordPatternField node) {
2414+
var fieldName = node.fieldName;
2415+
if (fieldName != null) {
2416+
var name = fieldName.name;
2417+
if (name != null) {
2418+
visitNamedNode(fieldName.name!, fieldName.colon, node.pattern);
2419+
} else {
2420+
// Named field with inferred name, like:
2421+
//
2422+
// var (:x) = (x: 1);
2423+
token(fieldName.colon);
2424+
visit(node.pattern);
2425+
}
2426+
} else {
2427+
visit(node.pattern);
2428+
}
23772429
}
23782430

23792431
@override
@@ -2485,6 +2537,12 @@ class SourceVisitor extends ThrowingAstVisitor {
24852537
token(node.rethrowKeyword);
24862538
}
24872539

2540+
@override
2541+
void visitRestPatternElement(RestPatternElement node) {
2542+
token(node.operator);
2543+
visit(node.pattern);
2544+
}
2545+
24882546
@override
24892547
void visitReturnStatement(ReturnStatement node) {
24902548
_simpleStatement(node, () {
@@ -2505,8 +2563,10 @@ class SourceVisitor extends ThrowingAstVisitor {
25052563

25062564
@override
25072565
void visitSetOrMapLiteral(SetOrMapLiteral node) {
2508-
_visitCollectionLiteral(
2509-
node, node.leftBracket, node.elements, node.rightBracket);
2566+
_visitCollectionLiteral(node.leftBracket, node.elements, node.rightBracket,
2567+
constKeyword: node.constKeyword,
2568+
typeArguments: node.typeArguments,
2569+
splitOuterCollection: true);
25102570
}
25112571

25122572
@override
@@ -2642,11 +2702,13 @@ class SourceVisitor extends ThrowingAstVisitor {
26422702
token(node.keyword);
26432703
space();
26442704

2705+
builder.indent();
26452706
builder.startBlockArgumentNesting();
26462707
builder.nestExpression();
26472708
visit(node.guardedPattern.pattern);
26482709
builder.unnest();
26492710
builder.endBlockArgumentNesting();
2711+
builder.unindent();
26502712

26512713
visit(node.guardedPattern.whenClause);
26522714
token(node.colon);
@@ -2927,20 +2989,31 @@ class SourceVisitor extends ThrowingAstVisitor {
29272989
/// split between the name and argument forces the argument list to split
29282990
/// too.
29292991
void visitNamedArgument(NamedExpression node, [NamedRule? rule]) {
2992+
visitNamedNode(
2993+
node.name.label.token, node.name.colon, node.expression, rule);
2994+
}
2995+
2996+
/// Visits syntax of the form `identifier: <node>`: a named argument or a
2997+
/// named record field.
2998+
void visitNamedNode(Token name, Token colon, AstNode node,
2999+
[NamedRule? rule]) {
29303000
builder.nestExpression();
29313001
builder.startSpan();
2932-
visit(node.name);
3002+
token(name);
3003+
token(colon);
29333004

29343005
// Don't allow a split between a name and a collection. Instead, we want
29353006
// the collection itself to split, or to split before the argument.
2936-
if (node.expression is ListLiteral || node.expression is SetOrMapLiteral) {
3007+
if (node is ListLiteral ||
3008+
node is SetOrMapLiteral ||
3009+
node is RecordLiteral) {
29373010
space();
29383011
} else {
29393012
var split = soloSplit();
29403013
if (rule != null) split.constrainWhenSplit(rule);
29413014
}
29423015

2943-
visit(node.expression);
3016+
visit(node);
29443017
builder.endSpan();
29453018
builder.unnest();
29463019
}
@@ -3207,26 +3280,67 @@ class SourceVisitor extends ThrowingAstVisitor {
32073280
}
32083281
}
32093282

3210-
/// Visits the collection literal [node] whose body starts with [leftBracket],
3283+
/// Visits the construct whose body starts with [leftBracket],
32113284
/// ends with [rightBracket] and contains [elements].
32123285
///
3213-
/// This is also used for argument lists with a trailing comma which are
3214-
/// considered "collection-like". In that case, [node] is `null`.
3215-
void _visitCollectionLiteral(Literal? node, Token leftBracket,
3216-
List<AstNode> elements, Token rightBracket,
3217-
[int? cost]) {
3218-
if (node is TypedLiteral) {
3219-
// See if `const` should be removed.
3220-
if (node.constKeyword != null &&
3221-
_constNesting > 0 &&
3222-
_formatter.fixes.contains(StyleFix.optionalConst)) {
3223-
// Don't lose comments before the discarded keyword, if any.
3224-
writePrecedingCommentsAndNewlines(node.constKeyword!);
3225-
} else {
3226-
modifier(node.constKeyword);
3286+
/// This is used for collection literals, collection patterns, and argument
3287+
/// lists with a trailing comma which are considered "collection-like".
3288+
///
3289+
/// If [splitOuterCollection] is `true` then this collection forces any
3290+
/// surrounding collections to split even if this one doesn't. We do this for
3291+
/// collection literals, but not other collection-like constructs.
3292+
void _visitCollectionLiteral(
3293+
Token leftBracket, List<AstNode> elements, Token rightBracket,
3294+
{Token? constKeyword,
3295+
TypeArgumentList? typeArguments,
3296+
int? cost,
3297+
bool splitOuterCollection = false,
3298+
bool isRecord = false}) {
3299+
// See if `const` should be removed.
3300+
if (constKeyword != null &&
3301+
_constNesting > 0 &&
3302+
_formatter.fixes.contains(StyleFix.optionalConst)) {
3303+
// Don't lose comments before the discarded keyword, if any.
3304+
writePrecedingCommentsAndNewlines(constKeyword);
3305+
} else {
3306+
modifier(constKeyword);
3307+
}
3308+
3309+
// Don't use the normal type argument list formatting code because we don't
3310+
// want to allow splitting before the "<" since there is no preceding
3311+
// identifier and it looks weird to have a "<" hanging by itself. Prevents:
3312+
//
3313+
// var list = <
3314+
// LongTypeName<
3315+
// TypeArgument,
3316+
// TypeArgument>>[];
3317+
if (typeArguments != null) {
3318+
builder.startSpan();
3319+
builder.nestExpression();
3320+
token(typeArguments.leftBracket);
3321+
builder.startRule(Rule(Cost.typeArgument));
3322+
3323+
for (var typeArgument in typeArguments.arguments) {
3324+
visit(typeArgument);
3325+
3326+
// Write the comma separator.
3327+
if (typeArgument != typeArguments.arguments.last) {
3328+
var comma = typeArgument.endToken.next;
3329+
3330+
// TODO(rnystrom): There is a bug in analyzer where the end token of a
3331+
// nullable record type is the ")" and not the "?". This works around
3332+
// that. Remove that's fixed.
3333+
if (comma?.lexeme == '?') comma = comma?.next;
3334+
3335+
token(comma);
3336+
split();
3337+
}
32273338
}
32283339

3229-
visit(node.typeArguments);
3340+
token(typeArguments.rightBracket);
3341+
builder.endRule();
3342+
builder.unnest();
3343+
builder.endSpan();
32303344
}
32313345

32323346
// Handle empty collections, with or without comments.
@@ -3236,7 +3350,7 @@ class SourceVisitor extends ThrowingAstVisitor {
32363350
}
32373351

32383352
// Unlike other collections, records don't force outer ones to split.
3239-
if (node is! RecordLiteral) {
3353+
if (splitOuterCollection) {
32403354
// Force all of the surrounding collections to split.
32413355
_collectionSplits.fillRange(0, _collectionSplits.length, true);
32423356

@@ -3245,7 +3359,7 @@ class SourceVisitor extends ThrowingAstVisitor {
32453359
}
32463360

32473361
_beginBody(leftBracket);
3248-
if (node is TypedLiteral) _startPossibleConstContext(node.constKeyword);
3362+
_startPossibleConstContext(constKeyword);
32493363

32503364
// If a collection contains a line comment, we assume it's a big complex
32513365
// blob of data with some documented structure. In that case, the user
@@ -3285,20 +3399,19 @@ class SourceVisitor extends ThrowingAstVisitor {
32853399
}
32863400
}
32873401

3288-
var force = false;
3289-
32903402
// If there is a collection inside this one, it forces this one to split.
3291-
if (node is! RecordLiteral) {
3403+
var force = false;
3404+
if (splitOuterCollection) {
32923405
force = _collectionSplits.removeLast();
32933406
}
32943407

32953408
// If the collection has a trailing comma, the user must want it to split.
32963409
// (Unless it's a single-element record literal, in which case the trailing
32973410
// comma is required for disambiguation.)
3298-
var isSingleElementRecord = node is RecordLiteral && elements.length == 1;
3411+
var isSingleElementRecord = isRecord && elements.length == 1;
32993412
if (elements.hasCommaAfter && !isSingleElementRecord) force = true;
33003413

3301-
if (node is TypedLiteral) _endPossibleConstContext(node.constKeyword);
3414+
_endPossibleConstContext(constKeyword);
33023415
_endBody(rightBracket, forceSplit: force);
33033416
}
33043417

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ environment:
99
sdk: ">=2.18.0 <3.0.0"
1010

1111
dependencies:
12-
analyzer: ^5.2.0
12+
analyzer: ^5.4.0
1313
args: ">=1.0.0 <3.0.0"
1414
path: ^1.0.0
1515
pub_semver: ">=1.4.4 <3.0.0"

0 commit comments

Comments
 (0)