Skip to content

Commit f9181eb

Browse files
authored
Format call chains! (#1356)
Format call chains! \o/ This handles unsplit call chains, fully split ones, as well as block-style calls, like: ``` target.call( argument, ); ``` This doesn't fully implement the proposal I sketched out because it only allows a single block call. There's a long TODO(tall) comment about whether we want to do that, but I figured it makes sense to do that as a separate iteration since there is so much in here. But it implements most of it, and I've migrated all of the existing tests over. It ended up being both simpler than the old formatter while also producing better output. The old formatter has a restriction where the indentation of a piece of code must be fixed regardless of which way it splits. It can't handle: ``` // Block style, so indent argument +2: target.property.call( argument ); // Can't do block style, so indent +6: target .slightlyLongerProperty .call( argument ); ``` That leads to it sometimes producing gross output like: ``` target .longProperty .anotherLongProperty .method( wtfIsGoingOnHere, ); ``` To try to avoid that, it has lots of heuristics during Chunk construction to try to not end up with output like that during line splitting. The new formatter doesn't need any of those heuristics because it can freely choose indentation during line splitting.
1 parent d8f04a9 commit f9181eb

File tree

13 files changed

+1221
-91
lines changed

13 files changed

+1221
-91
lines changed

lib/src/front_end/ast_node_visitor.dart

Lines changed: 25 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import '../dart_formatter.dart';
1212
import '../piece/adjacent.dart';
1313
import '../piece/assign.dart';
1414
import '../piece/block.dart';
15-
import '../piece/chain.dart';
1615
import '../piece/constructor.dart';
1716
import '../piece/for.dart';
1817
import '../piece/if.dart';
@@ -22,6 +21,7 @@ import '../piece/piece.dart';
2221
import '../piece/variable.dart';
2322
import '../source_code.dart';
2423
import 'adjacent_builder.dart';
24+
import 'chain_builder.dart';
2525
import 'comment_writer.dart';
2626
import 'delimited_list_builder.dart';
2727
import 'piece_factory.dart';
@@ -412,18 +412,9 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
412412

413413
@override
414414
Piece visitConstructorName(ConstructorName node) {
415-
// If there is an import prefix and/or constructor name, then allow
416-
// splitting before the `.`. This doesn't look good, but is consistent with
417-
// constructor calls that don't have `new` or `const`. We allow splitting
418-
// in the latter because there is no way to distinguish syntactically
419-
// between a named constructor call and any other kind of method call or
420-
// property access.
421-
var operations = <Piece>[];
422-
423415
var builder = AdjacentBuilder(this);
424416
if (node.type.importPrefix case var importPrefix?) {
425417
builder.token(importPrefix.name);
426-
operations.add(builder.build());
427418
builder.token(importPrefix.period);
428419
}
429420

@@ -435,16 +426,13 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
435426

436427
// If this is a named constructor, the name.
437428
if (node.name != null) {
438-
operations.add(builder.build());
439429
builder.token(node.period);
440430
builder.visit(node.name);
441431
}
442432

443433
// If there was a prefix or constructor name, then make a splittable piece.
444434
// Otherwise, the current piece is a simple identifier for the name.
445-
operations.add(builder.build());
446-
if (operations.length == 1) return operations.first;
447-
return ChainPiece(operations);
435+
return builder.build();
448436
}
449437

450438
@override
@@ -1027,35 +1015,19 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
10271015

10281016
@override
10291017
Piece visitIndexExpression(IndexExpression node) {
1030-
// TODO(tall): Allow splitting before and/or after the `[` when method
1031-
// chain formatting is fully implemented. For now, we just output the code
1032-
// so that tests of other language features that contain index expressions
1033-
// can run.
1034-
return buildPiece((b) {
1035-
b.visit(node.target);
1036-
b.token(node.leftBracket);
1037-
b.visit(node.index);
1038-
b.token(node.rightBracket);
1039-
});
1018+
Piece? targetPiece;
1019+
if (node.target case var target?) targetPiece = nodePiece(target);
1020+
return createIndexExpression(targetPiece, node);
10401021
}
10411022

10421023
@override
10431024
Piece visitInstanceCreationExpression(InstanceCreationExpression node) {
10441025
var builder = AdjacentBuilder(this);
10451026
builder.token(node.keyword, spaceAfter: true);
10461027

1047-
// If there is an import prefix and/or constructor name, then allow
1048-
// splitting before the `.`. This doesn't look good, but is consistent with
1049-
// constructor calls that don't have `new` or `const`. We allow splitting
1050-
// in the latter because there is no way to distinguish syntactically
1051-
// between a named constructor call and any other kind of method call or
1052-
// property access.
1053-
var operations = <Piece>[];
1054-
10551028
var constructor = node.constructorName;
10561029
if (constructor.type.importPrefix case var importPrefix?) {
10571030
builder.token(importPrefix.name);
1058-
operations.add(builder.build());
10591031
builder.token(importPrefix.period);
10601032
}
10611033

@@ -1066,19 +1038,13 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
10661038

10671039
// If this is a named constructor call, the name.
10681040
if (constructor.name case var name?) {
1069-
operations.add(builder.build());
10701041
builder.token(constructor.period);
10711042
builder.visit(name);
10721043
}
10731044

10741045
builder.visit(node.argumentList);
1075-
operations.add(builder.build());
10761046

1077-
if (operations.length > 1) {
1078-
return ChainPiece(operations);
1079-
} else {
1080-
return operations.first;
1081-
}
1047+
return builder.build();
10821048
}
10831049

10841050
@override
@@ -1195,15 +1161,23 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
11951161

11961162
@override
11971163
Piece visitMethodInvocation(MethodInvocation node) {
1198-
return buildPiece((b) {
1199-
// TODO(tall): Support splitting at `.` or `?.`. Right now we just format
1200-
// it inline so that we can use method calls in other tests.
1201-
b.visit(node.target);
1202-
b.token(node.operator);
1203-
b.visit(node.methodName);
1204-
b.visit(node.typeArguments);
1205-
b.visit(node.argumentList);
1206-
});
1164+
// If there's no target, this is a "bare" function call like "foo(1, 2)",
1165+
// or a section in a cascade.
1166+
//
1167+
// If it looks like a constructor or static call, we want to keep the
1168+
// target and method together instead of including the method in the
1169+
// subsequent method chain.
1170+
if (node.target == null || node.looksLikeStaticCall) {
1171+
return buildPiece((b) {
1172+
b.visit(node.target);
1173+
b.token(node.operator);
1174+
b.visit(node.methodName);
1175+
b.visit(node.typeArguments);
1176+
b.visit(node.argumentList);
1177+
});
1178+
}
1179+
1180+
return ChainBuilder(this, node).build();
12071181
}
12081182

12091183
@override
@@ -1354,14 +1328,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
13541328

13551329
@override
13561330
Piece visitPrefixedIdentifier(PrefixedIdentifier node) {
1357-
// TODO(tall): Allow splitting before the `.` when method chain formatting
1358-
// is fully implemented. For now, we just output the code so that tests
1359-
// of other language features that contain prefixed identifiers can run.
1360-
return buildPiece((b) {
1361-
b.visit(node.prefix);
1362-
b.token(node.period);
1363-
b.visit(node.identifier);
1364-
});
1331+
return ChainBuilder(this, node).build();
13651332
}
13661333

13671334
@override
@@ -1382,14 +1349,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
13821349

13831350
@override
13841351
Piece visitPropertyAccess(PropertyAccess node) {
1385-
// TODO(tall): Allow splitting before the `.` when method chain formatting
1386-
// is fully implemented. For now, we just output the code so that tests
1387-
// of other language features that contain property accesses can run.
1388-
return buildPiece((b) {
1389-
b.visit(node.target);
1390-
b.token(node.operator);
1391-
b.visit(node.propertyName);
1392-
});
1352+
return ChainBuilder(this, node).build();
13931353
}
13941354

13951355
@override

lib/src/front_end/chain_builder.dart

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analyzer/dart/ast/ast.dart';
6+
import 'package:analyzer/dart/ast/token.dart';
7+
8+
import '../ast_extensions.dart';
9+
import '../piece/chain.dart';
10+
import '../piece/piece.dart';
11+
import 'piece_factory.dart';
12+
13+
/// Creates [Chain] pieces from method calls and property accesses, along with
14+
/// postfix operations (`!`, index operators, and function invocation
15+
/// expressions) that follow them.
16+
///
17+
/// In the AST for method calls, selectors are nested bottom up such that this
18+
/// expression:
19+
///
20+
/// obj.a(1)[2].c(3)
21+
///
22+
/// Is structured like:
23+
///
24+
/// .c()
25+
/// / \
26+
/// [] 3
27+
/// / \
28+
/// .a() 2
29+
/// / \
30+
/// obj 1
31+
///
32+
/// This means visiting the AST from top down visits the selectors from right
33+
/// to left. It's easier to format if we organize them as a linear series of
34+
/// selectors from left to right. Further, we want to organize it into a
35+
/// two-tier hierarchy. We have an outer list of method calls and property
36+
/// accesses. Then each of those may have one or more postfix selectors
37+
/// attached: indexers, null-assertions, or invocations. This mirrors how they
38+
/// are formatted.
39+
///
40+
/// This lets us create a single [ChainPiece] for the entire series of dotted
41+
/// operations, so that we can control splitting them or not as a unit.
42+
class ChainBuilder {
43+
final PieceFactory _visitor;
44+
45+
/// The left-most target of the chain.
46+
late Piece _target;
47+
48+
/// Whether the target expression may contain newlines when the chain is not
49+
/// fully split. (It may always contain newlines when the chain splits.)
50+
///
51+
/// This is true for most expressions but false for delimited ones to avoid
52+
/// ugly formatting like:
53+
///
54+
/// function(
55+
/// argument,
56+
/// )
57+
/// .method();
58+
late final bool _allowSplitInTarget;
59+
60+
/// The dotted property accesses and method calls following the target.
61+
final List<ChainCall> _calls = [];
62+
63+
ChainBuilder(this._visitor, Expression expression) {
64+
_unwrapCall(expression);
65+
}
66+
67+
Piece build() {
68+
// If there are no calls, there's no chain.
69+
if (_calls.isEmpty) return _target;
70+
71+
// Count the number of contiguous properties at the beginning of the chain.
72+
var leadingProperties = 0;
73+
while (leadingProperties < _calls.length &&
74+
_calls[leadingProperties].type == CallType.property) {
75+
leadingProperties++;
76+
}
77+
78+
// See if there is a call that we can block format. It can either be the
79+
// very last call, if non-empty:
80+
//
81+
// target.property.method().last(
82+
// argument,
83+
// );
84+
//
85+
// Or the second-to-last if the last call can't split:
86+
//
87+
// target.property.method().penultimate(
88+
// argument,
89+
// ).toList();
90+
var blockCallIndex = switch (_calls) {
91+
[..., ChainCall(canSplit: true)] => _calls.length - 1,
92+
[..., ChainCall(canSplit: true), ChainCall(canSplit: false)] =>
93+
_calls.length - 2,
94+
_ => -1,
95+
};
96+
97+
return ChainPiece(_target, _calls, leadingProperties, blockCallIndex,
98+
allowSplitInTarget: _allowSplitInTarget);
99+
}
100+
101+
/// Given [expression], which is the outermost expression for some call chain,
102+
/// recursively traverses the selectors to fill in the list of [_calls].
103+
///
104+
/// Initializes [_target] with the innermost subexpression that isn't a part
105+
/// of the call chain. For example, given:
106+
///
107+
/// foo.bar()!.baz[0][1].bang()
108+
///
109+
/// This returns `foo` and fills [_calls] with:
110+
///
111+
/// .bar()!
112+
/// .baz[0][1]
113+
/// .bang()
114+
void _unwrapCall(Expression expression) {
115+
switch (expression) {
116+
case Expression(looksLikeStaticCall: true):
117+
// Don't include things that look like static method or constructor
118+
// calls in the call chain because that tends to split up named
119+
// constructors from their class.
120+
_visitTarget(expression);
121+
122+
// Selectors.
123+
case MethodInvocation(:var target?):
124+
_unwrapCall(target);
125+
126+
var callPiece = _visitor.buildPiece((b) {
127+
b.token(expression.operator);
128+
b.visit(expression.methodName);
129+
b.visit(expression.typeArguments);
130+
b.visit(expression.argumentList);
131+
});
132+
133+
var canSplit = expression.argumentList.arguments
134+
.canSplit(expression.argumentList.rightParenthesis);
135+
_calls.add(ChainCall(callPiece,
136+
canSplit ? CallType.splittableCall : CallType.unsplittableCall));
137+
138+
case PropertyAccess(:var target?):
139+
_unwrapCall(target);
140+
141+
var piece = _visitor.buildPiece((b) {
142+
b.token(expression.operator);
143+
b.visit(expression.propertyName);
144+
});
145+
146+
_calls.add(ChainCall(piece, CallType.property));
147+
148+
case PrefixedIdentifier(:var prefix):
149+
_unwrapCall(prefix);
150+
151+
var piece = _visitor.buildPiece((b) {
152+
b.token(expression.period);
153+
b.visit(expression.identifier);
154+
});
155+
156+
_calls.add(ChainCall(piece, CallType.property));
157+
158+
// Postfix expressions.
159+
case FunctionExpressionInvocation():
160+
_unwrapPostfix(expression.function, (target) {
161+
return _visitor.buildPiece((b) {
162+
b.add(target);
163+
b.visit(expression.typeArguments);
164+
b.visit(expression.argumentList);
165+
});
166+
});
167+
168+
case IndexExpression():
169+
_unwrapPostfix(expression.target!, (target) {
170+
return _visitor.createIndexExpression(target, expression);
171+
});
172+
173+
case PostfixExpression() when expression.operator.type == TokenType.BANG:
174+
_unwrapPostfix(expression.operand, (target) {
175+
return _visitor.buildPiece((b) {
176+
b.add(target);
177+
b.token(expression.operator);
178+
});
179+
});
180+
181+
default:
182+
// Otherwise, it isn't a selector so we've reached the target.
183+
_visitTarget(expression);
184+
}
185+
}
186+
187+
/// Creates and stores the resulting Piece for [target] as well as whether it
188+
/// allows being split.
189+
void _visitTarget(Expression target) {
190+
_allowSplitInTarget = target.canBlockSplit;
191+
_target = _visitor.nodePiece(target);
192+
}
193+
194+
void _unwrapPostfix(
195+
Expression operand, Piece Function(Piece target) createPostfix) {
196+
_unwrapCall(operand);
197+
// If we don't have a preceding call to hang the postfix expression off of,
198+
// wrap it around the target expression. For example:
199+
//
200+
// (list + another)!
201+
if (_calls.isEmpty) {
202+
_target = createPostfix(_target);
203+
} else {
204+
_calls.last.wrapPostfix(createPostfix);
205+
}
206+
}
207+
}

0 commit comments

Comments
 (0)