Skip to content

Commit 885f1ec

Browse files
authored
Format if elements. (#1344)
Format if elements. I was hoping to be able to reuse more of the createIf() code in PieceFactory for both if elements and if statements, but there are enough differences between the two that that didn't work out, so I just inlined the code for both of them directly in AstNodeVisitor. I was able to reuse the existing IfPiece, though, which is nice.
1 parent fc1a229 commit 885f1ec

10 files changed

+1158
-100
lines changed

lib/src/ast_extensions.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,23 @@ extension AstNodeExtensions on AstNode {
9393

9494
return null;
9595
}
96+
97+
/// If this is a spread of a non-empty collection literal, then returns `this`
98+
/// as a [SpreadElement].
99+
///
100+
/// Otherwise, returns `null`.
101+
SpreadElement? get spreadCollection {
102+
var node = this;
103+
if (node is! SpreadElement) return null;
104+
105+
return switch (node.expression) {
106+
ListLiteral(:var elements, :var rightBracket) ||
107+
SetOrMapLiteral(:var elements, :var rightBracket)
108+
when elements.canSplit(rightBracket) =>
109+
node,
110+
_ => null,
111+
};
112+
}
96113
}
97114

98115
extension AstIterableExtensions on Iterable<AstNode> {

lib/src/back_end/solution.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ class Solution implements Comparable<Solution> {
160160
if (_nextPieceToExpand case var piece?) {
161161
return [
162162
for (var state in piece.states)
163-
if (_stateSet.tryBind(piece, state) case final stateSet?)
163+
if (_stateSet.tryBind(piece, state) case var stateSet?)
164164
Solution._(root, pageWidth, stateSet)
165165
];
166166
}

lib/src/front_end/ast_node_visitor.dart

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -935,12 +935,151 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
935935

936936
@override
937937
Piece visitIfElement(IfElement node) {
938-
throw UnimplementedError();
938+
var piece = IfPiece();
939+
940+
// Recurses through the else branches to flatten them into a linear if-else
941+
// chain handled by a single [IfPiece].
942+
void traverse(Token? precedingElse, IfElement ifElement) {
943+
var spreadThen = ifElement.thenElement.spreadCollection;
944+
945+
var condition = buildPiece((b) {
946+
b.token(precedingElse, spaceAfter: true);
947+
b.add(startControlFlow(ifElement.ifKeyword, ifElement.leftParenthesis,
948+
ifElement.expression, ifElement.rightParenthesis));
949+
950+
// Make the `...` part of the header so that IfPiece can correctly
951+
// constrain the inner collection literal's ListPiece to split.
952+
if (spreadThen != null) {
953+
b.space();
954+
b.token(spreadThen.spreadOperator);
955+
}
956+
});
957+
958+
Piece thenElement;
959+
if (spreadThen != null) {
960+
thenElement = nodePiece(spreadThen.expression);
961+
} else {
962+
thenElement = nodePiece(ifElement.thenElement);
963+
}
964+
965+
// If the then branch of an if element is itself a control flow
966+
// element, then force the outer if to always split.
967+
if (ifElement.thenElement.isControlFlowElement) {
968+
piece.pin(State.split);
969+
}
970+
971+
piece.add(condition, thenElement, isBlock: spreadThen != null);
972+
973+
switch (ifElement.elseElement) {
974+
case IfElement elseIf:
975+
// Hit an else-if, so flatten it into the chain with the `else`
976+
// becoming part of the next section's header.
977+
traverse(ifElement.elseKeyword, elseIf);
978+
979+
case var elseElement?:
980+
var spreadElse = elseElement.spreadCollection;
981+
982+
// Any other kind of else body ends the chain, with the header for
983+
// the last section just being the `else` keyword.
984+
var header = buildPiece((b) {
985+
b.token(ifElement.elseKeyword!);
986+
987+
// Make the `...` part of the header so that IfPiece can correctly
988+
// constrain the inner collection literal's ListPiece to split.
989+
if (spreadElse != null) {
990+
b.space();
991+
b.token(spreadElse.spreadOperator);
992+
}
993+
});
994+
995+
Piece statement;
996+
if (spreadElse != null) {
997+
statement = nodePiece(spreadElse.expression);
998+
} else {
999+
statement = nodePiece(elseElement);
1000+
}
1001+
1002+
piece.add(header, statement, isBlock: spreadElse != null);
1003+
1004+
// If the else branch of an if element is itself a control flow
1005+
// element, then force the outer if to always split.
1006+
if (ifElement.thenElement.isControlFlowElement) {
1007+
piece.pin(State.split);
1008+
}
1009+
1010+
case null:
1011+
break; // Nothing to do.
1012+
}
1013+
}
1014+
1015+
traverse(null, node);
1016+
return piece;
9391017
}
9401018

9411019
@override
9421020
Piece visitIfStatement(IfStatement node) {
943-
return createIf(node);
1021+
var piece = IfPiece();
1022+
1023+
// Recurses through the else branches to flatten them into a linear if-else
1024+
// chain handled by a single [IfPiece].
1025+
void traverse(Token? precedingElse, IfStatement ifStatement) {
1026+
var condition = buildPiece((b) {
1027+
b.token(precedingElse, spaceAfter: true);
1028+
b.add(startControlFlow(
1029+
ifStatement.ifKeyword,
1030+
ifStatement.leftParenthesis,
1031+
ifStatement.expression,
1032+
ifStatement.rightParenthesis));
1033+
b.space();
1034+
});
1035+
1036+
// Edge case: When the then branch is a block and there is an else clause
1037+
// after it, we want to force the block to split even if empty, like:
1038+
//
1039+
// ```
1040+
// if (condition) {
1041+
// } else {
1042+
// body;
1043+
// }
1044+
// ```
1045+
var thenStatement = switch (ifStatement.thenStatement) {
1046+
Block thenBlock when ifStatement.elseStatement != null =>
1047+
createBlock(thenBlock, forceSplit: true),
1048+
_ => nodePiece(ifStatement.thenStatement)
1049+
};
1050+
1051+
piece.add(condition, thenStatement,
1052+
isBlock: ifStatement.thenStatement is Block);
1053+
1054+
switch (ifStatement.elseStatement) {
1055+
case IfStatement elseIf:
1056+
// Hit an else-if, so flatten it into the chain with the `else`
1057+
// becoming part of the next section's header.
1058+
traverse(ifStatement.elseKeyword, elseIf);
1059+
1060+
case var elseStatement?:
1061+
// Any other kind of else body ends the chain, with the header for
1062+
// the last section just being the `else` keyword.
1063+
var header = buildPiece((b) {
1064+
b.token(ifStatement.elseKeyword, spaceAfter: true);
1065+
});
1066+
var statement = nodePiece(elseStatement);
1067+
piece.add(header, statement, isBlock: elseStatement is Block);
1068+
}
1069+
}
1070+
1071+
traverse(null, node);
1072+
1073+
// If statements almost always split at the clauses unless the if is a
1074+
// simple if with only a single unbraced then statement and no else clause,
1075+
// like:
1076+
//
1077+
// if (condition) print("ok");
1078+
if (node.thenStatement is Block || node.elseStatement != null) {
1079+
piece.pin(State.split);
1080+
}
1081+
1082+
return piece;
9441083
}
9451084

9461085
@override
@@ -1666,8 +1805,11 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
16661805

16671806
@override
16681807
Piece visitWhileStatement(WhileStatement node) {
1669-
var condition = startControlFlow(node.whileKeyword, node.leftParenthesis,
1670-
node.condition, node.rightParenthesis);
1808+
var condition = buildPiece((b) {
1809+
b.add(startControlFlow(node.whileKeyword, node.leftParenthesis,
1810+
node.condition, node.rightParenthesis));
1811+
b.space();
1812+
});
16711813

16721814
var body = nodePiece(node.body);
16731815

lib/src/front_end/piece_factory.dart

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import '../piece/assign.dart';
99
import '../piece/block.dart';
1010
import '../piece/clause.dart';
1111
import '../piece/function.dart';
12-
import '../piece/if.dart';
1312
import '../piece/infix.dart';
1413
import '../piece/list.dart';
1514
import '../piece/piece.dart';
@@ -405,59 +404,6 @@ mixin PieceFactory {
405404
return piece;
406405
}
407406

408-
// TODO(tall): Generalize this to work with if elements too.
409-
/// Creates a piece for a chain of if-else-if... statements.
410-
Piece createIf(IfStatement ifStatement) {
411-
var piece = IfPiece();
412-
413-
// Recurses through the else branches to flatten them into a linear if-else
414-
// chain handled by a single [IfPiece].
415-
void traverse(Piece? previousElse, IfStatement node) {
416-
var condition = buildPiece((b) {
417-
if (previousElse != null) b.add(previousElse);
418-
b.add(startControlFlow(node.ifKeyword, node.leftParenthesis,
419-
node.expression, node.rightParenthesis));
420-
});
421-
422-
// Edge case: When the then branch is a block and there is an else clause
423-
// after it, we want to force the block to split even if empty, like:
424-
//
425-
// ```
426-
// if (condition) {
427-
// } else {
428-
// body;
429-
// }
430-
// ```
431-
var thenStatement = switch (node.thenStatement) {
432-
Block thenBlock when node.elseStatement != null =>
433-
createBlock(thenBlock, forceSplit: true),
434-
_ => nodePiece(node.thenStatement)
435-
};
436-
437-
piece.add(condition, thenStatement, isBlock: node.thenStatement is Block);
438-
439-
switch (node.elseStatement) {
440-
case IfStatement elseIf:
441-
// Hit an else-if, so flatten it into the chain with the `else`
442-
// becoming part of the next section's header.
443-
traverse(buildPiece((b) {
444-
b.token(node.elseKeyword);
445-
b.space();
446-
}), elseIf);
447-
448-
case var elseStatement?:
449-
// Any other kind of else body ends the chain, with the header for
450-
// the last section just being the `else` keyword.
451-
var header = tokenPiece(node.elseKeyword!);
452-
var statement = nodePiece(elseStatement);
453-
piece.add(header, statement, isBlock: elseStatement is Block);
454-
}
455-
}
456-
457-
traverse(null, ifStatement);
458-
return piece;
459-
}
460-
461407
/// Creates an [ImportPiece] for an import or export directive.
462408
Piece createImport(NamespaceDirective directive, Token keyword,
463409
{Token? deferredKeyword, Token? asKeyword, SimpleIdentifier? prefix}) {

lib/src/piece/if.dart

Lines changed: 30 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,76 +5,64 @@ import '../back_end/code_writer.dart';
55
import '../constants.dart';
66
import 'piece.dart';
77

8-
/// A piece for an if statement.
8+
/// A piece for an if statement or element.
99
///
1010
/// We also use this for while statements, which are formatted exactly like an
1111
/// if statement with no else clause.
1212
class IfPiece extends Piece {
1313
final List<_IfSection> _sections = [];
1414

15-
/// Whether the if is a simple if with only a single unbraced then statement
16-
/// and no else clause, like:
17-
///
18-
/// ```
19-
/// if (condition) print("ok");
20-
/// ```
21-
///
22-
/// Unlike other if statements, these allow a discretionary split after the
23-
/// condition.
24-
bool get _isUnbracedIfThen =>
25-
_sections.length == 1 && !_sections.single.isBlock;
26-
2715
void add(Piece header, Piece statement, {required bool isBlock}) {
2816
_sections.add(_IfSection(header, statement, isBlock));
2917
}
3018

31-
/// If there is at least one else or else-if clause, then it always splits.
3219
@override
33-
List<State> get additionalStates => [if (_isUnbracedIfThen) State.split];
20+
List<State> get additionalStates => [State.split];
3421

3522
@override
36-
void format(CodeWriter writer, State state) {
37-
if (_isUnbracedIfThen) {
38-
// A split in the condition or statement forces moving the entire
39-
// statement to the next line.
40-
writer.setAllowNewlines(state != State.unsplit);
23+
void applyConstraints(State state, Constrain constrain) {
24+
// If an if element, any spread collection's split state must follow the
25+
// surrounding if element's: we either split all the spreads or none of
26+
// them. And if any of the non-spread then or else branches split, then the
27+
// spreads do too. This has no effect on if statements since blocks always
28+
// split.
29+
for (var section in _sections) {
30+
if (section.isBlock) {
31+
constrain(section.statement, state);
32+
}
33+
}
34+
}
4135

42-
var section = _sections.single;
43-
writer.format(section.header);
44-
writer.splitIf(state == State.split, indent: Indent.block);
45-
writer.format(section.statement);
46-
return;
36+
@override
37+
void forEachChild(void Function(Piece piece) callback) {
38+
for (var section in _sections) {
39+
callback(section.header);
40+
callback(section.statement);
4741
}
42+
}
4843

44+
@override
45+
void format(CodeWriter writer, State state) {
4946
for (var i = 0; i < _sections.length; i++) {
5047
var section = _sections[i];
5148

49+
// A split in the condition forces the branches to split.
50+
writer.setAllowNewlines(state == State.split);
5251
writer.format(section.header);
5352

54-
// If the statement is a block, then keep the `{` on the same line as the
55-
// header part.
56-
if (section.isBlock) {
57-
writer.space();
58-
} else {
59-
writer.newline(indent: Indent.block);
53+
if (!section.isBlock) {
54+
writer.splitIf(state == State.split, indent: Indent.block);
6055
}
6156

6257
writer.format(section.statement);
6358

6459
// Reset the indentation for the subsequent `else` or `} else` line.
6560
if (i < _sections.length - 1) {
66-
writer.splitIf(!section.isBlock, indent: Indent.none);
61+
writer.splitIf(state == State.split && !section.isBlock,
62+
indent: Indent.none);
6763
}
6864
}
6965
}
70-
71-
@override
72-
void forEachChild(void Function(Piece piece) callback) {
73-
for (var section in _sections) {
74-
callback(section.header);
75-
callback(section.statement);
76-
}
77-
}
7866
}
7967

8068
/// A single section in a chain of if-elses.
@@ -88,7 +76,8 @@ class _IfSection {
8876
final Piece header;
8977
final Piece statement;
9078

91-
/// Whether the [statement] piece is from a block.
79+
/// Whether the [statement] piece is from a block or a spread collection
80+
/// literal.
9281
final bool isBlock;
9382

9483
_IfSection(this.header, this.statement, this.isBlock);

0 commit comments

Comments
 (0)