Skip to content

Commit 1208f9e

Browse files
Ensure comment formatting is idempotent. (#1610)
* Ensure comment formatting is idempotent. In some cases, a line comment can appear between two tokens that otherwise never split, like after "if (" and before the condition. That leads to some tricky edge case behavior. If you formatted: ```dart if ( // Comment condition) { ; } ``` It would see the newline before the comment and decide the comment was a "leading comment" which means it gets attached to the condition expression. Then the formatter would output it like: ```dart if (// Comment condition) { ; } ``` That's because leading comments don't write a newline before themselves. Then if you format that again, there's no newline before the `//`, so now it's a hanging comment. Hanging comments get a space before them, so you get: ```dart if ( // Comment condition) { ; } ``` Really, leading comments (as the name implies) are intended to always begin a line. So this PR makes sure they do that. While I was at it, I modified the test runner to run the formatter twice on *every* test to ensure that everything is idempotent. That doesn't *prove* that the formatter will always produce idempotent output, but it at least gives us pretty good test coverage that it *does* behave idempotent-ly. In practice, most normal looking code would never hit this edge case. You have to put a comment in an unusual spot where a split doesn't occur. This still feels like a fairly brittle part of the formatter to me. Comments appearing between tokens that never split otherwise is handled on a pretty ad hoc basis (which is why some of the tests in this PR have weird indentation). I'd like a cleaner more systematic solution, but I'm not sure what that would look like. Fix #1606. * Fix version number in CHANGELOG. * Update lib/src/back_end/code_writer.dart Co-authored-by: Nate Bosch <[email protected]> * Apply review feedback. --------- Co-authored-by: Nate Bosch <[email protected]>
1 parent c57d49c commit 1208f9e

File tree

11 files changed

+149
-35
lines changed

11 files changed

+149
-35
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.0.1-wip
2+
3+
* Ensure comment formatting is idempotent (#1606).
4+
15
## 3.0.0
26

37
This is a large change. Under the hood, the formatter was almost completely

lib/src/back_end/code_writer.dart

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -372,12 +372,13 @@ final class CodeWriter {
372372
// If we found a problematic line, and there is are pieces on the line that
373373
// we can try to split, then remember them so that the solution will expand
374374
// them next.
375-
if (!_foundExpandLine && (_column > _pageWidth || !_solution.isValid)) {
376-
// We found a problematic line, so remember the pieces on it.
377-
_foundExpandLine = true;
375+
if (_foundExpandLine) return;
376+
if (_currentLinePieces.isNotEmpty &&
377+
(_column > _pageWidth || !_solution.isValid)) {
378378
_expandPieces.addAll(_currentLinePieces);
379-
} else if (!_foundExpandLine) {
380-
// This line was OK, so we don't need to expand the piece on it.
379+
_foundExpandLine = true;
380+
} else {
381+
// This line was OK, so we don't need to expand the pieces on it.
381382
_currentLinePieces.clear();
382383
}
383384
}

lib/src/dart_formatter.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ final class DartFormatter {
124124
if (!source.isCompilationUnit) {
125125
var prefix = 'void foo() { ';
126126
inputOffset = prefix.length;
127-
text = '$prefix$text }';
127+
text = '$prefix$text\n }';
128128
unitSourceCode = SourceCode(
129129
text,
130130
uri: source.uri,

lib/src/piece/leading_comment.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ final class LeadingCommentPiece extends Piece {
3131

3232
@override
3333
void format(CodeWriter writer, State state) {
34+
// If a piece has a leading comment, that comment should not also be a
35+
// hanging comment, so ensure it begins its own line. This is also important
36+
// to ensure that formatting is idempotent: If we don't do this, a comment
37+
// might be a leading comment in the input and then get output on the same
38+
// line as some preceding code, which would lead it to be a hanging comment
39+
// the next time the formatter runs.
40+
writer.newline();
3441
for (var comment in _comments) {
3542
writer.format(comment);
3643
}

lib/src/testing/test_file.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,19 @@ final class TestFile {
137137
}
138138

139139
var isCompilationUnit = file.path.endsWith('.unit');
140+
141+
// The output always has a trailing newline. When formatting a statement,
142+
// the formatter (correctly) doesn't output trailing newlines when
143+
// formatting a statement, so remove it from the expectation to match.
144+
var outputText = outputBuffer.toString();
145+
if (!isCompilationUnit) {
146+
assert(outputText.endsWith('\n'));
147+
outputText = outputText.substring(0, outputText.length - 1);
148+
}
149+
140150
var input = _extractSelection(_unescapeUnicode(inputBuffer.toString()),
141151
isCompilationUnit: isCompilationUnit);
142-
var output = _extractSelection(_unescapeUnicode(outputBuffer.toString()),
152+
var output = _extractSelection(_unescapeUnicode(outputText),
143153
isCompilationUnit: isCompilationUnit);
144154

145155
tests.add(FormatTest(

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: dart_style
22
# Note: See tool/grind.dart for how to bump the version.
3-
version: 3.0.0
3+
version: 3.0.1-wip
44
description: >-
55
Opinionated, automatic Dart source code formatter.
66
Provides an API and a CLI tool.

test/tall/pattern/cast_comment.stmt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ if (obj case
3333
constant as Type) {;}
3434
<<<
3535
if (obj
36-
case // comment
36+
case
37+
// comment
3738
constant as Type) {
3839
;
3940
}

test/tall/regression/1606/1606.unit

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
>>>
2+
class AaaaAaaaAaaaAaaaaaaaAaaaaaaa {
3+
@aaaaaaaa
4+
AaaaaAaaaAaaaAaaaaaaa get aaaaAaaaAaaaaaaa => aaaaaaaaAaaaaaaaaAaaaaa
5+
? (AaaaaaaaaAaaAaaaaaaa()
6+
// AAA/Aaaaa aaaaaaaaa aaaaaaaa aaaa AAAAAAAA aaaaaaaaa aaaaaa aaaaaaaa.
7+
..aaaaAaaaaaaaaAaaaaaAaagetaaa =
8+
AaaaAaaaaaaaaAaaaaaAaagetaaa.aaaaAaaaaa(aaaaaaaa: [
9+
AaaaaaaaAaaaaaa()..aaaaaaaa = AaaaaaaaAA_Aaaaaaaa.AAAAA,
10+
], aaaaaaAaaaaa: [
11+
AaaaaaAaaaaaAaaaaaa()
12+
..aaaaaaAaaaaa = AaaaaaAaaaaa.AAAAAA_AAAAAAAAAA_AAAAAA,
13+
AaaaaaAaaaaaAaaaaaa()..aaaaaaAaaaaa = AaaaaaAaaaaa.AAAAAA_AAAA_AAAAAAA
14+
]))
15+
: AaaaAaaaaaaAaaaaaaAaaaaaaa();
16+
}
17+
<<<
18+
class AaaaAaaaAaaaAaaaaaaaAaaaaaaa {
19+
@aaaaaaaa
20+
AaaaaAaaaAaaaAaaaaaaa get aaaaAaaaAaaaaaaa =>
21+
aaaaaaaaAaaaaaaaaAaaaaa
22+
? (AaaaaaaaaAaaAaaaaaaa()
23+
// AAA/Aaaaa aaaaaaaaa aaaaaaaa aaaa AAAAAAAA aaaaaaaaa aaaaaa aaaaaaaa.
24+
..aaaaAaaaaaaaaAaaaaaAaagetaaa =
25+
AaaaAaaaaaaaaAaaaaaAaagetaaa.aaaaAaaaaa(
26+
aaaaaaaa: [
27+
AaaaaaaaAaaaaaa()..aaaaaaaa = AaaaaaaaAA_Aaaaaaaa.AAAAA,
28+
],
29+
aaaaaaAaaaaa: [
30+
AaaaaaAaaaaaAaaaaaa()
31+
..aaaaaaAaaaaa = AaaaaaAaaaaa.AAAAAA_AAAAAAAAAA_AAAAAA,
32+
AaaaaaAaaaaaAaaaaaa()
33+
..aaaaaaAaaaaa = AaaaaaAaaaaa.AAAAAA_AAAA_AAAAAAA,
34+
],
35+
))
36+
: AaaaAaaaaaaAaaaaaaAaaaaaaa();
37+
}

test/tall/statement/if_comment.stmt

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,44 @@ if (true) {
9191
body;
9292
} else {
9393
other;
94-
} // comment
94+
} // comment
95+
>>> Hanging line comment before infix condition.
96+
if (// comment
97+
a && b) { body; }
98+
<<<
99+
### The indentation is odd here because it's an odd place for a comment.
100+
if ( // comment
101+
a && b) {
102+
body;
103+
}
104+
>>> Non-hanging line comment before infix condition.
105+
if (
106+
// comment
107+
a && b) { body; }
108+
<<<
109+
### The indentation is odd here because it's an odd place for a comment.
110+
if (
111+
// comment
112+
a && b) {
113+
body;
114+
}
115+
>>> Hanging line comment before infix chain condition.
116+
if (// comment
117+
a && b && c) { body; }
118+
<<<
119+
### The indentation is odd here because it's an odd place for a comment.
120+
if ( // comment
121+
a && b && c) {
122+
body;
123+
}
124+
>>> Non-hanging line comment before infix chain condition.
125+
if (
126+
// comment
127+
a && b && c) { body; }
128+
<<<
129+
### The indentation is odd here because it's an odd place for a comment.
130+
if (
131+
// comment
132+
a && b && c) {
133+
body;
134+
}

test/tall/top_level/import_comment.unit

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import 'foo.dart'
77
hide
88
First, //
99
Second;
10-
>>> Don't split `==` because of leading comment before left operand.
10+
>>> Don't split `==` because of comment before left operand.
1111
import 'uri.dart' if (
1212
// comment
1313
config == 'value') 'c';
1414
<<<
15+
### The indentation is odd here because it's an odd place for a comment.
1516
import 'uri.dart'
16-
if (// comment
17+
if (
18+
// comment
1719
config == 'value') 'c';

0 commit comments

Comments
 (0)