Skip to content

Commit 7fd45d6

Browse files
authored
Don't crash on switches with empty trailing cases. (#1182)
* Don't crash on switches with empty trailing cases. In the process, I came up with a cleaner and faster solution for handling rules that span hard splits. Fix #1181. * Remove unneeded "late".
1 parent 35a5d9f commit 7fd45d6

File tree

9 files changed

+90
-104
lines changed

9 files changed

+90
-104
lines changed

lib/src/chunk.dart

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,8 @@ class Chunk extends Selection {
114114

115115
/// Whether this chunk marks the end of a range of chunks that can be line
116116
/// split independently of the following chunks.
117-
///
118-
/// You must call markDivide() before accessing this.
119117
bool get canDivide => _canDivide;
120-
late final bool _canDivide;
118+
bool _canDivide = true;
121119

122120
/// The number of characters in this chunk when unsplit.
123121
int get length => (_spaceWhenUnsplit ? 1 : 0) + _text.length;
@@ -177,9 +175,12 @@ class Chunk extends Selection {
177175
/// that [Rule].
178176
bool indentBlock(int Function(Rule) getValue) => false;
179177

180-
// Mark whether this chunk can divide the range of chunks.
181-
void markDivide(bool canDivide) {
182-
_canDivide = canDivide;
178+
/// Prevent the line splitter from diving at this chunk.
179+
///
180+
/// This should be called on any chunk where line splitting choices before
181+
/// and after this chunk relate to each other.
182+
void preventDivide() {
183+
_canDivide = false;
183184
}
184185

185186
@override

lib/src/chunk_builder.dart

Lines changed: 30 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,23 @@ class ChunkBuilder {
6060
/// Whether the next chunk should be flush left.
6161
bool _pendingFlushLeft = false;
6262

63+
/// Whether subsequent hard splits should be allowed to divide for line
64+
/// splitting.
65+
///
66+
/// Most rules used by multiple chunks will never have a hard split on a
67+
/// chunk between two chunks using that rule. That means when we see a hard
68+
/// split, we can line split the chunks before and after it independently.
69+
///
70+
/// However, there are a couple of places where a single rule spans
71+
/// multiple chunks where hard splits also appear interleaved between them.
72+
/// Currently, that's the rule for splitting switch case bodies, and the
73+
/// rule for parameter list metadata.
74+
///
75+
/// In those circumstances, we mark the chunk with the hard split as not
76+
/// being allowed to divide. That way, all of the chunks using the rule are
77+
/// split together.
78+
bool _pendingPreventDivide = false;
79+
6380
/// Whether the most recently written output was a comment.
6481
bool _afterComment = false;
6582

@@ -156,6 +173,7 @@ class ChunkBuilder {
156173

157174
_nesting.commitNesting();
158175
_afterComment = false;
176+
_pendingPreventDivide = false;
159177
}
160178

161179
/// Writes one or two hard newlines.
@@ -168,10 +186,14 @@ class ChunkBuilder {
168186
/// nesting. If [nest] is `true` then the next line will use expression
169187
/// nesting.
170188
void writeNewline(
171-
{bool isDouble = false, bool flushLeft = false, bool nest = false}) {
189+
{bool isDouble = false,
190+
bool flushLeft = false,
191+
bool nest = false,
192+
bool preventDivide = false}) {
172193
_pendingNewlines = isDouble ? 2 : 1;
173194
_pendingFlushLeft = flushLeft;
174195
_pendingNested = nest;
196+
_pendingPreventDivide |= preventDivide;
175197
}
176198

177199
/// Writes a space before the subsequent non-whitespace text.
@@ -855,10 +877,14 @@ class ChunkBuilder {
855877
isHard: isHard, isDouble: isDouble, space: space);
856878
}
857879

880+
if (chunk.rule.isHardened) {
881+
_handleHardSplit();
882+
883+
if (_pendingPreventDivide) chunk.preventDivide();
884+
}
885+
858886
_pendingNewlines = 0;
859887
_pendingNested = false;
860-
861-
if (chunk.rule.isHardened) _handleHardSplit();
862888
return chunk;
863889
}
864890

@@ -901,32 +927,9 @@ class ChunkBuilder {
901927
// splits, along with all of the other rules they constrain.
902928
_hardenRules();
903929

904-
var ruleRanges = _calculateRuleRanges();
905-
906930
for (var i = 0; i < _chunks.length; i++) {
907931
var chunk = _chunks[i];
908-
var canDivide = _canDivide(chunk);
909-
910-
if (canDivide) {
911-
// Don't divide in the middle of a rule.
912-
//
913-
// This rarely comes into play since few rules can have hard splits
914-
// between their chunks while not also being hardened themselves. But
915-
// rules for paramater metadata and switch case bodies can.
916-
for (var range in ruleRanges) {
917-
// Example:
918-
// Chunk index: 0 1 2 3 4 5
919-
// Rule: a a [2, 4]
920-
// Divide: | | |
921-
// 1 2 5
922-
if (i > range[0] && i < range[1]) {
923-
canDivide = false;
924-
break;
925-
}
926-
}
927-
}
928-
929-
chunk.markDivide(canDivide);
932+
if (!_canDivide(chunk)) chunk.preventDivide();
930933
}
931934
}
932935

@@ -949,61 +952,6 @@ class ChunkBuilder {
949952
return true;
950953
}
951954

952-
/// For each non-hardened rule, find the range of chunks that share it.
953-
///
954-
/// We must ensure that those chunks are all split together so that they
955-
/// don't try to pick different values for the same rule.
956-
///
957-
/// Returns a list of `[low, high]` ranges.
958-
///
959-
/// Very few rules actually allow a hard split in between chunks sharing that
960-
/// rule (as of today just switch case bodies and parameter metadata). To
961-
/// avoid returning a huge list of mostly useless ranges, we only return
962-
/// ranges for rules that contain at least one hard split between their first
963-
/// and last chunks.
964-
List<List<int>> _calculateRuleRanges() {
965-
var ruleStarts = <Rule, int>{};
966-
var ruleRanges = <Rule, List<int>>{};
967-
var lastHardSplit = -1;
968-
var usefulRanges = <Rule, List<int>>{};
969-
970-
for (var i = 0; i < _chunks.length; i++) {
971-
var rule = _chunks[i].rule;
972-
if (!rule.isHardened) {
973-
var start = ruleStarts[rule];
974-
if (start == null) {
975-
// This is the first chunk using this rule. Don't create a range yet
976-
// until we see it used by another chunk, since most rules are only
977-
// used for a single chunk.
978-
ruleStarts[rule] = i;
979-
} else {
980-
// This rule now spans at least two chunks, so create a range. The
981-
// low end is the first index using this rule rule.
982-
var range = ruleRanges.putIfAbsent(rule, () => [start, 0]);
983-
984-
// Keep updating the high end of the range as long we continue to find
985-
// the rule.
986-
range[1] = i;
987-
988-
// If we encounter a hard split within this rule's range, keep the
989-
// range.
990-
if (lastHardSplit > range[0] && lastHardSplit < range[1]) {
991-
usefulRanges[rule] = range;
992-
}
993-
}
994-
} else {
995-
lastHardSplit = i;
996-
}
997-
}
998-
999-
// TODO(rnystrom): There is probably something more efficient we can do.
1000-
// We could merge overlapping ranges in the list into larger ones. Or maybe
1001-
// during chunk building, when a Rule sets `splitsOnInnerRules` to `false`
1002-
// and we write a hard chunk, we can mark that hard chunk as not splittable.
1003-
1004-
return usefulRanges.values.toList();
1005-
}
1006-
1007955
/// Hardens the active rules when a hard split occurs within them.
1008956
void _handleHardSplit() {
1009957
if (_rules.isEmpty) return;

lib/src/dart_formatter.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ class DartFormatter {
199199
// brittle), we just try parsing everything with patterns enabled. When a
200200
// parse error occurs, we try parsing it again with pattern disabled. If that
201201
// happens to parse without error, then we use that result instead.
202-
ParseStringResult _parse(String source, String? uri, {required bool patterns}) {
202+
ParseStringResult _parse(String source, String? uri,
203+
{required bool patterns}) {
203204
// Enable all features that are enabled by default in the current analyzer
204205
// version.
205206
var featureSet = FeatureSet.fromEnableFlags2(

lib/src/debug.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,6 @@ void dumpChunks(int start, List<Chunk> chunks) {
7676

7777
addSpans(chunks);
7878

79-
var spans = spanSet.toList();
80-
var rules = chunks.map((chunk) => chunk.rule).toSet();
81-
8279
var rows = <List<String>>[];
8380

8481
void addChunk(List<Chunk> chunks, String prefix, int index) {
@@ -108,6 +105,7 @@ void dumpChunks(int start, List<Chunk> chunks) {
108105
if (rule.isHardened) ruleString += '!';
109106
row.add(ruleString);
110107

108+
var rules = chunks.map((chunk) => chunk.rule).toSet();
111109
var constrainedRules = rule.constrainedRules.toSet().intersection(rules);
112110
writeIf(
113111
constrainedRules.isNotEmpty, () => "-> ${constrainedRules.join(" ")}");
@@ -123,6 +121,7 @@ void dumpChunks(int start, List<Chunk> chunks) {
123121
writeIf(chunk.indent != 0, () => 'indent ${chunk.indent}');
124122
writeIf(chunk.nesting.indent != 0, () => 'nest ${chunk.nesting}');
125123

124+
var spans = spanSet.toList();
126125
if (spans.length <= 20) {
127126
var spanBars = '';
128127
for (var span in spans) {

lib/src/source_visitor.dart

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2834,13 +2834,13 @@ class SourceVisitor extends ThrowingAstVisitor {
28342834

28352835
// We don't want the split between cases to force them to split.
28362836
caseRule.disableSplitOnInnerRules();
2837-
oneOrTwoNewlines();
2837+
oneOrTwoNewlines(preventDivide: true);
28382838
} else {
28392839
// We don't want the split between cases to force them to split.
28402840
caseRule.disableSplitOnInnerRules();
28412841

28422842
// Don't preserve blank lines between empty cases.
2843-
newline();
2843+
builder.writeNewline(preventDivide: true);
28442844
}
28452845
}
28462846

@@ -3640,7 +3640,7 @@ class SourceVisitor extends ThrowingAstVisitor {
36403640
builder = builder.startBlock();
36413641

36423642
for (var parameter in parameters.parameters) {
3643-
newline();
3643+
builder.writeNewline(preventDivide: true);
36443644
visit(parameter);
36453645
_writeCommaAfter(parameter);
36463646

@@ -3657,7 +3657,7 @@ class SourceVisitor extends ThrowingAstVisitor {
36573657
var firstDelimiter =
36583658
parameters.rightDelimiter ?? parameters.rightParenthesis;
36593659
if (firstDelimiter.precedingComments != null) {
3660-
newline();
3660+
builder.writeNewline(preventDivide: true);
36613661
writePrecedingCommentsAndNewlines(firstDelimiter);
36623662
}
36633663

@@ -4120,12 +4120,9 @@ class SourceVisitor extends ThrowingAstVisitor {
41204120
/// Allow either one or two newlines to be emitted before the next
41214121
/// non-whitespace token based on whether any blank lines exist in the source
41224122
/// between the last token and the next one.
4123-
void oneOrTwoNewlines() {
4124-
if (_linesBeforeNextToken > 1) {
4125-
twoNewlines();
4126-
} else {
4127-
newline();
4128-
}
4123+
void oneOrTwoNewlines({bool preventDivide = false}) {
4124+
builder.writeNewline(
4125+
isDouble: _linesBeforeNextToken > 1, preventDivide: preventDivide);
41294126
}
41304127

41314128
/// The number of newlines between the last written token and the next one to

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ environment:
99
sdk: ">=2.18.0 <3.0.0"
1010

1111
dependencies:
12-
analyzer: ^5.5.0
12+
analyzer: ^5.6.0
1313
args: ">=1.0.0 <3.0.0"
1414
path: ^1.0.0
1515
pub_semver: ">=1.4.4 <3.0.0"

test/comments/functions.unit

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,18 @@ function({
234234
a,
235235
}) {
236236
;
237-
}
237+
}
238+
>>> metadata in trailing comma parameter list splits together even with comment
239+
function(@Annotation longParameter,
240+
// Comment.
241+
@Annotation @Other @Third longParameter2,) {}
242+
<<<
243+
function(
244+
@Annotation
245+
longParameter,
246+
// Comment.
247+
@Annotation
248+
@Other
249+
@Third
250+
longParameter2,
251+
) {}

test/comments/switch.stmt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,21 @@ switch (n) {
8787
case 2:
8888
// comment 2
8989
}
90+
>>> bodies all split or don't together even with comment in the middle
91+
switch (n) {
92+
case 0: longBodyExpression + thatForcesSplit;
93+
// comment
94+
case 1: c;
95+
}
96+
<<<
97+
switch (n) {
98+
case 0:
99+
longBodyExpression +
100+
thatForcesSplit;
101+
// comment
102+
case 1:
103+
c;
104+
}
90105
>>> keeps one blank line around case comments in switch expression
91106
e = switch (n) {
92107

test/regression/1100/1181.stmt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
>>>
2+
switch (e) {
3+
case E.e1:
4+
break;
5+
case E.e2:
6+
}
7+
<<<
8+
switch (e) {
9+
case E.e1: break;
10+
case E.e2:
11+
}

0 commit comments

Comments
 (0)