Skip to content

Commit 582a717

Browse files
authored
Format if and return statements. (#1284)
* Format if and return statements. I also tweaked the CodeWriter API to make it easier to update the indentation when inserting a newline. This doesn't do anything radically new, but makes the piece formatting code a little shorter. Made a field in SequencePiece private that didn't need to be public. * Fix class comment.
1 parent 6fd92fb commit 582a717

File tree

10 files changed

+369
-32
lines changed

10 files changed

+369
-32
lines changed

lib/src/back_end/code_writer.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,13 @@ class CodeWriter {
141141

142142
/// Inserts a line split in the output.
143143
///
144-
/// If [blank] is true, writes an extra newline to produce a blank line.
145-
void newline({bool blank = false}) {
144+
/// If [blank] is `true`, writes an extra newline to produce a blank line.
145+
///
146+
/// If [indent] is given, set the indentation of the new line (and all
147+
/// subsequent lines) to that indentation relative to the containing piece.
148+
void newline({bool blank = false, int? indent}) {
149+
if (indent != null) setIndent(indent);
150+
146151
handleNewline();
147152
_finishLine();
148153
_buffer.writeln();

lib/src/front_end/ast_node_visitor.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
143143

144144
@override
145145
void visitBlock(Block node) {
146-
createBlock(node.leftBracket, node.statements, node.rightBracket);
146+
createBlock(node);
147147
}
148148

149149
@override
@@ -441,7 +441,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
441441

442442
@override
443443
void visitIfStatement(IfStatement node) {
444-
throw UnimplementedError();
444+
createIf(node);
445445
}
446446

447447
@override
@@ -766,7 +766,14 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
766766

767767
@override
768768
void visitReturnStatement(ReturnStatement node) {
769-
throw UnimplementedError();
769+
token(node.returnKeyword);
770+
771+
if (node.expression case var expression) {
772+
writer.space();
773+
visit(expression);
774+
}
775+
776+
token(node.semicolon);
770777
}
771778

772779
@override

lib/src/front_end/piece_factory.dart

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:analyzer/dart/ast/token.dart';
77
import '../ast_extensions.dart';
88
import '../piece/assign.dart';
99
import '../piece/block.dart';
10+
import '../piece/if.dart';
1011
import '../piece/import.dart';
1112
import '../piece/infix.dart';
1213
import '../piece/piece.dart';
@@ -46,33 +47,42 @@ mixin PieceFactory implements CommentWriter {
4647

4748
/// Creates a [BlockPiece] for a given bracket-delimited block or declaration
4849
/// body.
49-
void createBlock(Token leftBracket, List<AstNode> nodes, Token rightBracket) {
50+
///
51+
/// If [forceSplit] is `true`, then the block will split even if empty. This
52+
/// is used, for example, with empty blocks in `if` statements followed by
53+
/// `else` clauses:
54+
///
55+
/// ```
56+
/// if (condition) {
57+
/// } else {}
58+
/// ```
59+
void createBlock(Block block, {bool forceSplit = false}) {
5060
// Edge case: If the block is completely empty, output it as simple
51-
// unsplittable text.
52-
if (nodes.isEmptyBody(rightBracket)) {
53-
token(leftBracket);
54-
token(rightBracket);
61+
// unsplittable text unless it's forced to split.
62+
if (block.statements.isEmptyBody(block.rightBracket) && !forceSplit) {
63+
token(block.leftBracket);
64+
token(block.rightBracket);
5565
return;
5666
}
5767

58-
token(leftBracket);
68+
token(block.leftBracket);
5969
var leftBracketPiece = writer.pop();
6070
writer.split();
6171

6272
var sequence = SequenceBuilder(this);
63-
for (var node in nodes) {
73+
for (var node in block.statements) {
6474
sequence.add(node);
6575
}
6676

6777
// Place any comments before the "}" inside the block.
68-
sequence.addCommentsBefore(rightBracket);
78+
sequence.addCommentsBefore(block.rightBracket);
6979

70-
token(rightBracket);
80+
token(block.rightBracket);
7181
var rightBracketPiece = writer.pop();
7282

7383
writer.push(BlockPiece(
7484
leftBracketPiece, sequence.build(), rightBracketPiece,
75-
alwaysSplit: nodes.isNotEmpty));
85+
alwaysSplit: forceSplit || block.statements.isNotEmpty));
7686
}
7787

7888
/// Creates a [ListPiece] for a collection literal.
@@ -123,6 +133,69 @@ mixin PieceFactory implements CommentWriter {
123133
}
124134
}
125135

136+
// TODO(tall): Generalize this to work with if elements too.
137+
/// Creates a piece for a chain of if-else-if... statements.
138+
void createIf(IfStatement ifStatement) {
139+
var piece = IfPiece();
140+
141+
// Recurses through the else branches to flatten them into a linear if-else
142+
// chain handled by a single [IfPiece].
143+
void traverse(IfStatement node) {
144+
token(node.ifKeyword);
145+
writer.space();
146+
token(node.leftParenthesis);
147+
visit(node.expression);
148+
token(node.rightParenthesis);
149+
var condition = writer.pop();
150+
writer.split();
151+
152+
// Edge case: When the then branch is a block and there is an else clause
153+
// after it, we want to force the block to split even if empty, like:
154+
//
155+
// ```
156+
// if (condition) {
157+
// } else {
158+
// body;
159+
// }
160+
// ```
161+
if (node.thenStatement case Block thenBlock
162+
when node.elseStatement != null) {
163+
createBlock(thenBlock, forceSplit: true);
164+
} else {
165+
visit(node.thenStatement);
166+
}
167+
168+
var thenStatement = writer.pop();
169+
writer.split();
170+
piece.add(condition, thenStatement, isBlock: node.thenStatement is Block);
171+
172+
switch (node.elseStatement) {
173+
case IfStatement elseIf:
174+
// Hit an else-if, so flatten it into the chain with the `else`
175+
// becoming part of the next section's header.
176+
token(node.elseKeyword);
177+
writer.space();
178+
traverse(elseIf);
179+
180+
case var elseStatement?:
181+
// Any other kind of else body ends the chain, with the header for
182+
// the last section just being the `else` keyword.
183+
token(node.elseKeyword);
184+
var header = writer.pop();
185+
writer.split();
186+
187+
visit(elseStatement);
188+
var statement = writer.pop();
189+
writer.split();
190+
piece.add(header, statement, isBlock: elseStatement is Block);
191+
}
192+
}
193+
194+
traverse(ifStatement);
195+
196+
writer.push(piece);
197+
}
198+
126199
/// Creates an [ImportPiece] for an import or export directive.
127200
void createImport(NamespaceDirective directive, Token keyword,
128201
{Token? deferredKeyword, Token? asKeyword, SimpleIdentifier? prefix}) {

lib/src/piece/assign.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ class AssignPiece extends Piece {
7272
if (state != _insideValue) writer.setIndent(Indent.expression);
7373

7474
writer.format(target);
75-
7675
writer.splitIf(state == _atEquals);
7776
writer.format(value);
7877
}

lib/src/piece/block.dart

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@ class BlockPiece extends Piece {
3737
writer.format(leftBracket);
3838

3939
if (_alwaysSplit || state == State.split) {
40-
writer.setIndent(Indent.block);
41-
writer.newline();
42-
writer.format(contents);
43-
writer.setIndent(Indent.none);
44-
writer.newline();
40+
if (contents.isNotEmpty) {
41+
writer.newline(indent: Indent.block);
42+
writer.format(contents);
43+
}
44+
45+
writer.newline(indent: Indent.none);
4546
} else {
4647
writer.setAllowNewlines(false);
4748
writer.format(contents);

lib/src/piece/if.dart

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) 2023, 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+
import '../back_end/code_writer.dart';
5+
import '../constants.dart';
6+
import 'piece.dart';
7+
8+
/// A piece for an if statement.
9+
class IfPiece extends Piece {
10+
final List<_IfSection> _sections = [];
11+
12+
/// Whether the if is a simple if with only a single unbraced then statement
13+
/// and no else clause, like:
14+
///
15+
/// ```
16+
/// if (condition) print("ok");
17+
/// ```
18+
///
19+
/// Unlike other if statements, these allow a discretionary split after the
20+
/// condition.
21+
bool get _isUnbracedIfThen =>
22+
_sections.length == 1 && !_sections.single.isBlock;
23+
24+
void add(Piece header, Piece statement, {required bool isBlock}) {
25+
_sections.add(_IfSection(header, statement, isBlock));
26+
}
27+
28+
/// If there is at least one else or else-if clause, then it always splits.
29+
@override
30+
List<State> get states => _isUnbracedIfThen ? const [State.split] : const [];
31+
32+
@override
33+
void format(CodeWriter writer, State state) {
34+
if (_isUnbracedIfThen) {
35+
// A split in the condition or statement forces moving the entire
36+
// statement to the next line.
37+
writer.setAllowNewlines(state != State.initial);
38+
39+
var section = _sections.single;
40+
writer.format(section.header);
41+
writer.splitIf(state == State.split, indent: Indent.block);
42+
writer.format(section.statement);
43+
return;
44+
}
45+
46+
for (var i = 0; i < _sections.length; i++) {
47+
var section = _sections[i];
48+
49+
writer.format(section.header);
50+
51+
// If the statement is a block, then keep the `{` on the same line as the
52+
// header part.
53+
if (section.isBlock) {
54+
writer.space();
55+
} else {
56+
writer.newline(indent: Indent.block);
57+
}
58+
59+
writer.format(section.statement);
60+
61+
// Reset the indentation for the subsequent `else` or `} else` line.
62+
if (i < _sections.length - 1) {
63+
writer.splitIf(!section.isBlock, indent: Indent.none);
64+
}
65+
}
66+
}
67+
68+
@override
69+
void forEachChild(void Function(Piece piece) callback) {
70+
for (var section in _sections) {
71+
callback(section.header);
72+
callback(section.statement);
73+
}
74+
}
75+
76+
@override
77+
String toString() => 'If';
78+
}
79+
80+
/// A single section in a chain of if-elses.
81+
///
82+
/// For the first then branch, the [header] is the `if (condition)` part and
83+
/// the statement is the then branch. For all `else if` branches, the [header]
84+
/// is the `else if (condition)` and the statement is the subsequent then
85+
/// branch. For the final `else` branch, if there is one, the [header] is just
86+
/// `else` and the statement is the else branch.
87+
class _IfSection {
88+
final Piece header;
89+
final Piece statement;
90+
91+
/// Whether the [statement] piece is from a block.
92+
final bool isBlock;
93+
94+
_IfSection(this.header, this.statement, this.isBlock);
95+
}

lib/src/piece/list.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@ class ListPiece extends Piece {
7070

7171
case State.split:
7272
// Each argument on its own line with a trailing comma after the last.
73-
writer.setIndent(Indent.block);
74-
writer.newline();
73+
writer.newline(indent: Indent.block);
7574
for (var i = 0; i < _arguments.length; i++) {
7675
var argument = _arguments[i];
7776
argument.format(writer,
@@ -80,8 +79,7 @@ class ListPiece extends Piece {
8079
writer.newline(blank: _blanksAfter.contains(argument));
8180
}
8281
}
83-
writer.setIndent(Indent.none);
84-
writer.newline();
82+
writer.newline(indent: Indent.none);
8583
}
8684

8785
writer.setAllowNewlines(true);

lib/src/piece/sequence.dart

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,34 @@ import 'piece.dart';
1111
/// Usually constructed using a [SequenceBuilder].
1212
class SequencePiece extends Piece {
1313
/// The series of members or statements.
14-
final List<Piece> contents;
14+
final List<Piece> _contents;
1515

1616
/// The pieces that should have a blank line preserved between them and the
1717
/// next piece.
1818
final Set<Piece> _blanksAfter;
1919

20-
SequencePiece(this.contents, this._blanksAfter);
20+
SequencePiece(this._contents, this._blanksAfter);
21+
22+
/// Whether this sequence has any contents.
23+
bool get isNotEmpty => _contents.isNotEmpty;
2124

2225
@override
2326
List<State> get states => const [];
2427

2528
@override
2629
void format(CodeWriter writer, State state) {
27-
for (var i = 0; i < contents.length; i++) {
28-
writer.format(contents[i]);
30+
for (var i = 0; i < _contents.length; i++) {
31+
writer.format(_contents[i]);
2932

30-
if (i < contents.length - 1) {
31-
writer.newline(blank: _blanksAfter.contains(contents[i]));
33+
if (i < _contents.length - 1) {
34+
writer.newline(blank: _blanksAfter.contains(_contents[i]));
3235
}
3336
}
3437
}
3538

3639
@override
3740
void forEachChild(void Function(Piece piece) callback) {
38-
contents.forEach(callback);
41+
_contents.forEach(callback);
3942
}
4043

4144
@override

0 commit comments

Comments
 (0)