Skip to content

Commit 5ffaab7

Browse files
authored
Refactor how child chunks are stored. (#1117)
* Refactor how child chunks are stored. Given a piece of code like: main() { a(); b(); } There are top level chunks for `main() }` and `}`, and the chunks for `a()` and `b()` are children. Previously, the code stored those child chunks in the preceding parent chunk (so `main() {` here). But it's the subsequent chunk (`}`) that determines whether the block contents are actually split, so that doesn't make a lot of sense and leads to weird `+ 1` and `- 1` when working with nested chunks. This refactors the code so that child chunks are stored on the same chunk that determines whether or not they split. This means that chunks are now written in a post-order traversal: a block chunk's children come before its own text. Since we now know that a chunk will have children at the moment that it's created, removed the old ChunkBlock class and replaced it with a BlockChunk subclass of Chunk. Also added a bunch of tests around trailing whitespace in blocks. When I was testing this change on a corpus of code, I thought it inadvertently changed some behavior, but it turns out that it was the previous refactoring (which did deliberately change block formatting) and not this one. These tests help pin that behavior down. This commit here has zero changes on the formatter's visible behavior. * Apply review feedback.
1 parent fb93b7c commit 5ffaab7

18 files changed

+340
-186
lines changed

lib/src/chunk.dart

Lines changed: 66 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,6 @@ class Chunk extends Selection {
8989
/// argument));
9090
final NestingLevel nesting;
9191

92-
/// If this chunk marks the beginning of a block, this contains the child
93-
/// chunks and other data about that nested block.
94-
///
95-
/// This should only be accessed when [isBlock] is `true`.
96-
ChunkBlock get block => _block!;
97-
ChunkBlock? _block;
98-
99-
/// Whether this chunk has a [block].
100-
bool get isBlock => _block != null;
101-
10292
/// The [Rule] that controls when a split should occur before this chunk.
10393
///
10494
/// Multiple splits may share a [Rule].
@@ -136,16 +126,7 @@ class Chunk extends Selection {
136126
///
137127
/// Does not include this chunk's own length, just the length of its child
138128
/// block chunks (recursively).
139-
int get unsplitBlockLength {
140-
if (!isBlock) return 0;
141-
142-
var length = 0;
143-
for (var chunk in block.chunks) {
144-
length += chunk.length + chunk.unsplitBlockLength;
145-
}
146-
147-
return length;
148-
}
129+
int get unsplitBlockLength => 0;
149130

150131
/// The [Span]s that contain this chunk.
151132
final spans = <Span>[];
@@ -189,29 +170,12 @@ class Chunk extends Selection {
189170
if (space != null) _spaceWhenUnsplit = space;
190171
}
191172

192-
/// Turns this chunk into one that can contain a block of child chunks.
193-
void makeBlock(Chunk? blockArgument) {
194-
assert(_block == null);
195-
_block = ChunkBlock(blockArgument);
196-
}
197-
198-
/// Returns `true` if the block body owned by this chunk should be expression
199-
/// indented given a set of rule values provided by [getValue].
200-
bool indentBlock(int Function(Rule) getValue) {
201-
if (!isBlock) return false;
202-
203-
var argument = block.argument;
204-
if (argument == null) return false;
205-
206-
var rule = argument.rule;
207-
208-
// There may be no rule if the block occurs inside a string interpolation.
209-
// In that case, it's not clear if anything will look particularly nice, but
210-
// expression nesting is probably marginally better.
211-
if (rule == Rule.dummy) return true;
212-
213-
return rule.isSplit(getValue(rule), argument);
214-
}
173+
/// Returns `true` if this chunk is a block whose children should be
174+
/// expression indented given a set of rule values provided by [getValue].
175+
///
176+
/// [getValue] takes a [Rule] and returns the chosen split state value for
177+
/// that [Rule].
178+
bool indentBlock(int Function(Rule) getValue) => false;
215179

216180
// Mark whether this chunk can divide the range of chunks.
217181
void markDivide(bool canDivide) {
@@ -234,9 +198,33 @@ class Chunk extends Selection {
234198
}
235199
}
236200

237-
/// The child chunks owned by a chunk that begins a "block" -- an actual block
238-
/// statement, function expression, or collection literal.
239-
class ChunkBlock {
201+
/// A [Chunk] containing a list of nested "child" chunks that are formatted
202+
/// independently of the surrounding chunks.
203+
///
204+
/// This is used for blocks, function expressions, collection literals, etc.
205+
/// Basically, anywhere we have a delimited body of code whose formatting
206+
/// doesn't depend on how the surrounding code is formatted except to determine
207+
/// indentation.
208+
///
209+
/// This chunk's own text is the closing delimiter of the block, so its
210+
/// children come before itself. For example, given this code:
211+
///
212+
/// main() {
213+
/// var list = [
214+
/// element,
215+
/// ];
216+
/// }
217+
///
218+
/// It is organized into a tree of chunks like so:
219+
///
220+
/// - Chunk "main() {"
221+
/// - BlockChunk
222+
/// |- Chunk "var list = ["
223+
/// |- BlockChunk
224+
/// | |- Chunk "element,"
225+
/// | '- (text) "];"
226+
/// '- (text) "}"
227+
class BlockChunk extends Chunk {
240228
/// If this block is for a collection literal in an argument list, this will
241229
/// be the chunk preceding this literal argument.
242230
///
@@ -245,9 +233,39 @@ class ChunkBlock {
245233
final Chunk? argument;
246234

247235
/// The child chunks in this block.
248-
final List<Chunk> chunks = [];
236+
final List<Chunk> children = [];
237+
238+
BlockChunk(this.argument, super.rule, super.indent, super.nesting,
239+
{required super.space, required super.flushLeft})
240+
: super(isDouble: false);
249241

250-
ChunkBlock(this.argument);
242+
/// The unsplit length of all of this chunk's block contents.
243+
///
244+
/// Does not include this chunk's own length, just the length of its child
245+
/// block chunks (recursively).
246+
@override
247+
int get unsplitBlockLength {
248+
var length = 0;
249+
for (var chunk in children) {
250+
length += chunk.length + chunk.unsplitBlockLength;
251+
}
252+
253+
return length;
254+
}
255+
256+
@override
257+
bool indentBlock(int Function(Rule) getValue) {
258+
var argument = this.argument;
259+
if (argument == null) return false;
260+
261+
// There may be no rule if the block occurs inside a string interpolation.
262+
// In that case, it's not clear if anything will look particularly nice, but
263+
// expression nesting is probably marginally better.
264+
var rule = argument.rule;
265+
if (rule == Rule.dummy) return true;
266+
267+
return rule.isSplit(getValue(rule), argument);
268+
}
251269
}
252270

253271
/// The in-progress state for a [Span] that has been started but has not yet

lib/src/chunk_builder.dart

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -567,28 +567,43 @@ class ChunkBuilder {
567567
_blockArgumentNesting.removeLast();
568568
}
569569

570-
/// Starts a new block as a child of the current chunk.
570+
/// Starts a new block chunk and returns the [ChunkBuilder] for it.
571571
///
572572
/// Nested blocks are handled using their own independent [LineWriter].
573-
ChunkBuilder startBlock([Chunk? argumentChunk]) {
574-
var chunk = _chunks.last;
575-
chunk.makeBlock(argumentChunk);
573+
ChunkBuilder startBlock(
574+
{Chunk? argumentChunk, bool indent = true, bool space = false}) {
575+
// Start a block chunk for the block. It will contain the chunks for the
576+
// contents of the block, and its own text will be the closing block
577+
// delimiter.
578+
var chunk = BlockChunk(argumentChunk, _rules.last, _nesting.indentation,
579+
_blockArgumentNesting.last,
580+
space: space, flushLeft: _pendingFlushLeft);
581+
_chunks.add(chunk);
582+
_pendingFlushLeft = false;
583+
584+
var builder = ChunkBuilder._(this, _formatter, _source, chunk.children);
576585

577-
return ChunkBuilder._(this, _formatter, _source, chunk.block.chunks);
586+
if (indent) builder.indent();
587+
588+
// Create a hard split for the contents. The rule on the parent BlockChunk
589+
// determines whether the body is split or not. This hard rule is only when
590+
// the block's contents are split.
591+
var rule = Rule.hard();
592+
builder.startRule(rule);
593+
builder.split(nest: false, space: space);
594+
595+
return builder;
578596
}
579597

580598
/// Ends this [ChunkBuilder], which must have been created by [startBlock()].
581599
///
582600
/// Forces the chunk that owns the block to split if it can tell that the
583601
/// block contents will always split. It does that by looking for hard splits
584-
/// in the block. If [bodyRule] is given, that rule will be ignored when
585-
/// determining if a block contains a hard split. If [space] is `true`, the
586-
/// split at the end of the block will get a space when unsplit. If
602+
/// in the block that aren't for top level elements in the block. If
587603
/// [forceSplit] is `true`, the block always splits.
588604
///
589605
/// Returns the previous writer for the surrounding block.
590-
ChunkBuilder endBlock(
591-
{Rule? bodyRule, bool space = false, bool forceSplit = true}) {
606+
ChunkBuilder endBlock({bool forceSplit = true}) {
592607
_divideChunks();
593608

594609
// If the last chunk ends with a comment that wants a newline after it,
@@ -606,37 +621,21 @@ class ChunkBuilder {
606621
break;
607622
}
608623

609-
// If there are any hardened splits in the chunks (aside from the first
610-
// one which is always a hard split since it is the beginning of the
611-
// code), then force the collection to split.
612-
if (chunk != _chunks.first &&
613-
chunk.rule.isHardened &&
614-
chunk.rule != bodyRule) {
624+
// If there are any hardened splits in the chunks (aside from ones
625+
// using the initial hard rule created by [startBlock()] which are for
626+
// the top level elements in the block), then force the block to split.
627+
if (chunk.rule.isHardened && chunk.rule != _rules.first) {
615628
forceSplit = true;
616629
break;
617630
}
618631
}
619632
}
620633

621-
if (forceSplit) forceRules();
622-
623-
var parent = _parent!;
624-
parent._endChildBlock(space: space, forceSplit: forceSplit);
625-
return parent;
626-
}
627-
628-
/// Finishes off the last chunk in a child block of this parent.
629-
void _endChildBlock({required bool space, required bool forceSplit}) {
630634
// If there is a hard newline within the block, force the surrounding rule
631635
// for it so that we apply that constraint.
632-
if (forceSplit) forceRules();
633-
634-
// Start a new chunk for the code after the block contents. The split at
635-
// the beginning of this chunk also determines whether the preceding block
636-
// splits and, if so, how it is indented.
637-
_startChunk(_blockArgumentNesting.last, isHard: false, space: space);
638-
639-
if (rule.isHardened) _handleHardSplit();
636+
var parent = _parent!;
637+
if (forceSplit) parent.forceRules();
638+
return parent;
640639
}
641640

642641
/// Finishes writing and returns a [SourceCode] containing the final output
@@ -902,9 +901,9 @@ class ChunkBuilder {
902901
if (!chunk.rule.isHardened) return false;
903902
if (chunk.nesting.isNested) return false;
904903

905-
// If the previous chunk is a block, then this chunk determines whether the
906-
// block contents split, so don't separate it from the block.
907-
if (i > 0 && _chunks[i - 1].isBlock) return false;
904+
// If the chunk is the ending delimiter of a block, then don't separate it
905+
// and its children from the preceding beginning of the block.
906+
if (_chunks[i] is BlockChunk) return false;
908907

909908
return true;
910909
}

lib/src/debug.dart

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ void dumpChunks(int start, List<Chunk> chunks) {
7070
for (var chunk in chunks) {
7171
spanSet.addAll(chunk.spans);
7272

73-
if (chunk.isBlock) addSpans(chunk.block.chunks);
73+
if (chunk is BlockChunk) addSpans(chunk.children);
7474
}
7575
}
7676

@@ -82,11 +82,17 @@ void dumpChunks(int start, List<Chunk> chunks) {
8282
var rows = <List<String>>[];
8383

8484
void addChunk(List<Chunk> chunks, String prefix, int index) {
85+
var chunk = chunks[index];
86+
87+
if (chunk is BlockChunk) {
88+
for (var j = 0; j < chunk.children.length; j++) {
89+
addChunk(chunk.children, '$prefix$index.', j);
90+
}
91+
}
92+
8593
var row = <String>[];
8694
row.add('$prefix$index:');
8795

88-
var chunk = chunks[index];
89-
9096
void writeIf(predicate, String Function() callback) {
9197
if (predicate) {
9298
row.add(callback());
@@ -148,12 +154,6 @@ void dumpChunks(int start, List<Chunk> chunks) {
148154
}
149155

150156
rows.add(row);
151-
152-
if (chunk.isBlock) {
153-
for (var j = 0; j < chunk.block.chunks.length; j++) {
154-
addChunk(chunk.block.chunks, '$prefix$index.', j);
155-
}
156-
}
157157
}
158158

159159
for (var i = start; i < chunks.length; i++) {
@@ -222,10 +222,11 @@ void dumpLines(List<Chunk> chunks, SplitSet splits) {
222222
void writeChunksUnsplit(List<Chunk> chunks) {
223223
for (var chunk in chunks) {
224224
if (chunk.spaceWhenUnsplit) buffer.write(' ');
225-
buffer.write(chunk.text);
226225

227226
// Recurse into the block.
228-
if (chunk.isBlock) writeChunksUnsplit(chunk.block.chunks);
227+
if (chunk is BlockChunk) writeChunksUnsplit(chunk.children);
228+
229+
buffer.write(chunk.text);
229230
}
230231
}
231232

@@ -241,11 +242,11 @@ void dumpLines(List<Chunk> chunks, SplitSet splits) {
241242
buffer.write(' ');
242243
}
243244

244-
buffer.write(chunk.text);
245-
246-
if (chunk.isBlock && !splits.shouldSplitAt(i)) {
247-
writeChunksUnsplit(chunk.block.chunks);
245+
if (chunk is BlockChunk && !splits.shouldSplitAt(i)) {
246+
writeChunksUnsplit(chunk.children);
248247
}
248+
249+
buffer.write(chunk.text);
249250
}
250251

251252
log(buffer);

lib/src/line_splitting/solve_state.dart

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ class SolveState {
297297
// And any expression nesting.
298298
indent += chunk.nesting.totalUsedIndent;
299299

300-
if (i > 0 && _splitter.chunks[i - 1].indentBlock(getValue)) {
300+
if (_splitter.chunks[i].indentBlock(getValue)) {
301301
indent += Indent.expression;
302302
}
303303
}
@@ -389,19 +389,18 @@ class SolveState {
389389
if (chunk.spaceWhenUnsplit) length++;
390390
}
391391

392-
length += chunk.text.length;
393-
394-
if (chunk.isBlock) {
395-
if (_splits.shouldSplitAt(i + 1)) {
392+
if (chunk is BlockChunk) {
393+
if (_splits.shouldSplitAt(i)) {
396394
// Include the cost of the nested block.
397-
cost += _splitter.writer
398-
.formatBlock(chunk, _splits.getColumn(i + 1))
399-
.cost;
395+
cost +=
396+
_splitter.writer.formatBlock(chunk, _splits.getColumn(i)).cost;
400397
} else {
401398
// Include the nested block inline, if any.
402399
length += chunk.unsplitBlockLength;
403400
}
404401
}
402+
403+
length += chunk.text.length;
405404
}
406405

407406
// Add the costs for the rules that have any splits.

0 commit comments

Comments
 (0)