Skip to content

Commit 317cb1f

Browse files
authored
Format pattern variable declarations, including in for loops. (#1392)
Format pattern variable declarations, including in for loops. This required pretty significantly revamping AssignPiece since now there can be complex syntax on the left side of the `=` too. It went through several iterations but I think I managed to simplify it down to something reasonable that still passes all of the tests. This PR doesn't do pattern assignment, but that should hopefully be a trivial change after this.
1 parent 794f6d7 commit 317cb1f

File tree

7 files changed

+382
-64
lines changed

7 files changed

+382
-64
lines changed

lib/src/front_end/ast_node_visitor.dart

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
384384
if (node.redirectedConstructor case var constructor?) {
385385
redirect = AssignPiece(
386386
tokenPiece(node.separator!), nodePiece(constructor),
387-
allowInnerSplit: false);
387+
canBlockSplitRight: false);
388388
} else if (node.initializers.isNotEmpty) {
389389
initializerSeparator = tokenPiece(node.separator!);
390390
initializers = createList(node.initializers,
@@ -586,7 +586,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
586586
var expression = nodePiece(node.expression);
587587

588588
b.add(AssignPiece(operatorPiece, expression,
589-
allowInnerSplit: node.expression.canBlockSplit));
589+
canBlockSplitRight: node.expression.canBlockSplit));
590590
b.token(node.semicolon);
591591
});
592592
}
@@ -1394,17 +1394,11 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
13941394

13951395
@override
13961396
Piece visitPatternVariableDeclaration(PatternVariableDeclaration node) {
1397-
// TODO(tall): This is just a basic implementation for the metadata tests.
1398-
// It still needs a full implementation and tests.
13991397
return buildPiece((b) {
14001398
b.metadata(node.metadata);
14011399
b.token(node.keyword);
14021400
b.space();
1403-
b.visit(node.pattern);
1404-
b.space();
1405-
b.token(node.equals);
1406-
b.space();
1407-
b.visit(node.expression);
1401+
b.add(createAssignment(node.pattern, node.equals, node.expression));
14081402
});
14091403
}
14101404

@@ -1879,7 +1873,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
18791873
var initializerPiece = nodePiece(initializer, commaAfter: true);
18801874

18811875
variables.add(AssignPiece(variablePiece, initializerPiece,
1882-
allowInnerSplit: initializer.canBlockSplit));
1876+
canBlockSplitRight: initializer.canBlockSplit));
18831877
} else {
18841878
variables.add(tokenPiece(variable.name, commaAfter: true));
18851879
}

lib/src/front_end/piece_factory.dart

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ mixin PieceFactory {
235235
ForPartsWithDeclarations(variables: AstNode? initializer):
236236
case ForParts forParts &&
237237
ForPartsWithExpression(initialization: AstNode? initializer):
238+
case ForParts forParts &&
239+
ForPartsWithPattern(variables: AstNode? initializer):
238240
// In a C-style for loop, treat the for loop parts like an argument list
239241
// where each clause is a separate argument. This means that when they
240242
// split, they split like:
@@ -285,9 +287,6 @@ mixin PieceFactory {
285287
partsList.rightBracket(rightParenthesis);
286288
return partsList.build();
287289

288-
case ForPartsWithPattern():
289-
throw UnimplementedError();
290-
291290
case ForEachParts forEachParts &&
292291
ForEachPartsWithDeclaration(loopVariable: AstNode variable):
293292
case ForEachParts forEachParts &&
@@ -335,12 +334,22 @@ mixin PieceFactory {
335334
b.token(leftParenthesis);
336335
b.add(createAssignment(
337336
variable, forEachParts.inKeyword, forEachParts.iterable,
338-
splitBeforeOperator: true, allowInnerSplit: true));
337+
splitBeforeOperator: true, canBlockSplitLeft: true));
339338
b.token(rightParenthesis);
340339
});
341340

342-
case ForEachPartsWithPattern():
343-
throw UnimplementedError();
341+
case ForEachParts forEachParts &&
342+
ForEachPartsWithPattern(:var keyword, :var pattern):
343+
return buildPiece((b) {
344+
b.token(leftParenthesis);
345+
b.token(keyword);
346+
b.space();
347+
348+
b.add(createAssignment(
349+
pattern, forEachParts.inKeyword, forEachParts.iterable,
350+
splitBeforeOperator: true));
351+
b.token(rightParenthesis);
352+
});
344353
}
345354
}
346355

@@ -958,47 +967,64 @@ mixin PieceFactory {
958967
/// of the next line when it splits. Otherwise, puts the operator at the end
959968
/// of the preceding line.
960969
///
961-
/// If [allowInnerSplit] is `true`, then a newline inside the target or
962-
/// right-hand side doesn't force splitting at the operator itself.
963-
Piece createAssignment(AstNode target, Token operator, AstNode rightHandSide,
970+
/// If [canBlockSplitLeft] is `true`, then the left-hand operand supports
971+
/// being block-formatted without indenting it farther, like:
972+
///
973+
/// var [
974+
/// element,
975+
/// ] = list;
976+
Piece createAssignment(
977+
AstNode leftHandSide, Token operator, AstNode rightHandSide,
964978
{bool splitBeforeOperator = false,
965979
bool includeComma = false,
966980
bool spaceBeforeOperator = true,
967-
bool allowInnerSplit = false}) {
968-
// If the right-hand side can have block formatting, then a newline in
969-
// it doesn't force the operator to split, as in:
981+
bool canBlockSplitLeft = false}) {
982+
// If an operand can have block formatting, then a newline in it doesn't
983+
// force the operator to split, as in:
984+
//
985+
// var [
986+
// element,
987+
// ] = list;
988+
//
989+
// Or:
970990
//
971991
// var list = [
972992
// element,
973993
// ];
974-
allowInnerSplit |= switch (rightHandSide) {
994+
canBlockSplitLeft |= switch (leftHandSide) {
995+
Expression() => leftHandSide.canBlockSplit,
996+
DartPattern() => leftHandSide.canBlockSplit,
997+
_ => false
998+
};
999+
1000+
var canBlockSplitRight = switch (rightHandSide) {
9751001
Expression() => rightHandSide.canBlockSplit,
9761002
DartPattern() => rightHandSide.canBlockSplit,
9771003
_ => false
9781004
};
9791005

1006+
Piece leftPiece;
1007+
Piece rightPiece;
9801008
if (splitBeforeOperator) {
981-
var targetPiece = nodePiece(target);
1009+
leftPiece = nodePiece(leftHandSide);
9821010

983-
var initializer = buildPiece((b) {
1011+
rightPiece = buildPiece((b) {
9841012
b.token(operator);
9851013
b.space();
9861014
b.visit(rightHandSide, commaAfter: includeComma);
9871015
});
988-
989-
return AssignPiece(targetPiece, initializer,
990-
allowInnerSplit: allowInnerSplit);
9911016
} else {
992-
var targetPiece = buildPiece((b) {
993-
b.visit(target);
1017+
leftPiece = buildPiece((b) {
1018+
b.visit(leftHandSide);
9941019
b.token(operator, spaceBefore: spaceBeforeOperator);
9951020
});
9961021

997-
var initializer = nodePiece(rightHandSide, commaAfter: includeComma);
998-
999-
return AssignPiece(targetPiece, initializer,
1000-
allowInnerSplit: allowInnerSplit);
1022+
rightPiece = nodePiece(rightHandSide, commaAfter: includeComma);
10011023
}
1024+
1025+
return AssignPiece(leftPiece, rightPiece,
1026+
canBlockSplitLeft: canBlockSplitLeft,
1027+
canBlockSplitRight: canBlockSplitRight);
10021028
}
10031029

10041030
/// Creates a piece for a parameter-like constructor: Either a simple formal

lib/src/piece/assign.dart

Lines changed: 102 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,37 +19,75 @@ import 'piece.dart';
1919
///
2020
/// var x = 123;
2121
///
22-
/// If the value is a delimited "block-like" expression, then we allow splitting
23-
/// inside the value but not at the `=` with no additional indentation:
22+
/// This state also allows splitting the right side if it can be block
23+
/// formatted:
2424
///
2525
/// var list = [
2626
/// element,
2727
/// ];
2828
///
29-
/// [_atOperator] Split after the `=`:
29+
/// [_blockSplit] Block split in the operands that support it and expression
30+
/// split in the others. This state requires at least one operand to support
31+
/// block splitting. Examples:
3032
///
31-
/// var name =
32-
/// longValueExpression;
33+
/// var [
34+
/// element,
35+
/// ] = value;
36+
///
37+
/// var [
38+
/// element,
39+
/// ] = longOperand +
40+
/// anotherOperand;
41+
///
42+
/// var (longVariable &&
43+
/// anotherVariable) = [
44+
/// element,
45+
/// ];
46+
///
47+
/// [_atOperator] Split at the `=` or `in` operator and allow expression
48+
/// splitting in either operand:
49+
///
50+
/// var (longVariable &&
51+
/// anotherVariable) =
52+
/// longOperand +
53+
/// anotherOperand;
3354
class AssignPiece extends Piece {
34-
/// Split after the operator.
55+
/// Split at the operator.
3556
///
36-
/// This is more costly because it's generally better to split either in the
37-
/// value (if it's delimited) or in the target.
38-
static const State _atOperator = State(2, cost: 2);
57+
/// This is more costly because it's generally better to block split in one
58+
/// or both of the operands.
59+
static const State _atOperator = State(1, cost: 2);
60+
61+
/// Block split in one or both of the operands.
62+
static const State _blockSplit = State(2);
3963

4064
/// The left-hand side of the operation. Includes the operator unless it is
4165
/// `in`.
42-
final Piece target;
66+
final Piece _left;
4367

4468
/// The right-hand side of the operation.
45-
final Piece value;
69+
final Piece _right;
4670

47-
/// Whether a newline is allowed in the right-hand side without forcing a
48-
/// split at the assignment operator.
49-
final bool _allowInnerSplit;
71+
/// If `true`, then the left side supports being block-formatted, like:
72+
///
73+
/// var [
74+
/// element1,
75+
/// element2,
76+
/// ] = value;
77+
final bool _canBlockSplitLeft;
5078

51-
AssignPiece(this.target, this.value, {bool allowInnerSplit = false})
52-
: _allowInnerSplit = allowInnerSplit;
79+
/// If `true` then the right side supports being block-formatted, like:
80+
///
81+
/// var list = [
82+
/// element1,
83+
/// element2,
84+
/// ];
85+
final bool _canBlockSplitRight;
86+
87+
AssignPiece(this._left, this._right,
88+
{bool canBlockSplitLeft = false, bool canBlockSplitRight = false})
89+
: _canBlockSplitLeft = canBlockSplitLeft,
90+
_canBlockSplitRight = canBlockSplitRight;
5391

5492
// TODO(tall): The old formatter allows the first operand of a split
5593
// conditional expression to be on the same line as the `=`, as in:
@@ -87,29 +125,66 @@ class AssignPiece extends Piece {
87125
// operand;
88126

89127
@override
90-
List<State> get additionalStates => [_atOperator];
128+
List<State> get additionalStates => [
129+
// If at least one operand can block split, allow splitting in operands
130+
// without splitting at the operator.
131+
if (_canBlockSplitLeft || _canBlockSplitRight) _blockSplit,
132+
_atOperator,
133+
];
91134

92135
@override
93136
void format(CodeWriter writer, State state) {
94-
// A split in either child piece forces splitting at assignment operator
95-
// unless specifically allowed.
96-
writer.pushAllowNewlines(_allowInnerSplit || state != State.unsplit);
137+
var allowNewlinesInLeft = true;
138+
var indentLeft = false;
139+
var allowNewlinesInRight = true;
140+
var indentRight = false;
141+
var collapseIndent = false;
142+
143+
switch (state) {
144+
case State.unsplit:
145+
allowNewlinesInLeft = false;
146+
147+
// Always allow block-splitting the right side if it supports it.
148+
allowNewlinesInRight = _canBlockSplitRight;
149+
150+
case _atOperator:
151+
// When splitting at the operator, both operands may split or not and
152+
// will be indented if they do.
153+
indentLeft = true;
154+
indentRight = true;
97155

98-
// Don't indent a split delimited expression.
99-
if (state != State.unsplit) writer.pushIndent(Indent.expression);
156+
case _blockSplit:
157+
// Indent either operand if it doesn't block split.
158+
indentLeft = !_canBlockSplitLeft;
159+
indentRight = !_canBlockSplitRight;
160+
collapseIndent = true;
161+
}
100162

101-
writer.format(target);
163+
writer.pushAllowNewlines(allowNewlinesInLeft);
164+
if (indentLeft) {
165+
writer.pushIndent(Indent.expression, canCollapse: collapseIndent);
166+
}
167+
168+
writer.format(_left);
102169
writer.splitIf(state == _atOperator);
103-
writer.format(value);
104170

105-
if (state != State.unsplit) writer.popIndent();
171+
if (indentLeft) writer.popIndent();
172+
writer.popAllowNewlines();
173+
174+
writer.pushAllowNewlines(allowNewlinesInRight);
175+
if (indentRight) {
176+
writer.pushIndent(Indent.expression, canCollapse: collapseIndent);
177+
}
178+
179+
writer.format(_right);
106180

181+
if (indentRight) writer.popIndent();
107182
writer.popAllowNewlines();
108183
}
109184

110185
@override
111186
void forEachChild(void Function(Piece piece) callback) {
112-
callback(target);
113-
callback(value);
187+
callback(_left);
188+
callback(_right);
114189
}
115190
}

test/expression/collection_for.stmt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,30 @@ var l = [for (;;) [if (c) d]];
138138
>>>
139139
var l = [for (;;) [for (c in d) e]];
140140
<<<
141-
var l = [for (;;) [for (c in d) e]];
141+
var l = [for (;;) [for (c in d) e]];
142+
>>> Pattern for-in.
143+
var list = [
144+
for (var (longIdentifier && anotherLongOne) in obj) element
145+
];
146+
<<<
147+
var list = [
148+
for (var (longIdentifier &&
149+
anotherLongOne)
150+
in obj)
151+
element,
152+
];
153+
>>> Pattern for.
154+
var list = [
155+
for (var (longIdentifier && anotherLongOne) = obj; cond; inc) element
156+
];
157+
<<<
158+
var list = [
159+
for (
160+
var (longIdentifier &&
161+
anotherLongOne) =
162+
obj;
163+
cond;
164+
inc
165+
)
166+
element,
167+
];

0 commit comments

Comments
 (0)