Skip to content

Commit cfbbf18

Browse files
committed
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.
1 parent c57d49c commit cfbbf18

File tree

10 files changed

+147
-36
lines changed

10 files changed

+147
-36
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.0-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: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -372,13 +372,15 @@ 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;
378-
_expandPieces.addAll(_currentLinePieces);
379-
} else if (!_foundExpandLine) {
380-
// This line was OK, so we don't need to expand the piece on it.
381-
_currentLinePieces.clear();
375+
if (!_foundExpandLine) {
376+
if (_currentLinePieces.isNotEmpty &&
377+
(_column > _pageWidth || !_solution.isValid)) {
378+
_expandPieces.addAll(_currentLinePieces);
379+
_foundExpandLine = true;
380+
} else {
381+
// This line was OK, so we don't need to expand the pieces on it.
382+
_currentLinePieces.clear();
383+
}
382384
}
383385
}
384386
}

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
}

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
}
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';

test/utils.dart

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -134,34 +134,52 @@ void _testFile(TestFile testFile) {
134134
indent: formatTest.leadingIndent,
135135
experimentFlags: const ['macros']);
136136

137-
var actual = formatter.formatSource(formatTest.input);
138-
139-
// The test files always put a newline at the end of the expectation.
140-
// Statements from the formatter (correctly) don't have that, so add
141-
// one to line up with the expected result.
142-
var actualText = actual.text;
143-
if (!testFile.isCompilationUnit) actualText += '\n';
144-
145-
// Fail with an explicit message because it's easier to read than
146-
// the matcher output.
147-
if (actualText != formatTest.output.text) {
148-
fail('Formatting did not match expectation. Expected:\n'
149-
'${formatTest.output.text}\nActual:\n$actualText');
150-
} else if (actual.selectionStart != formatTest.output.selectionStart ||
151-
actual.selectionLength != formatTest.output.selectionLength) {
152-
fail('Selection did not match expectation. Expected:\n'
153-
'${formatTest.output.textWithSelectionMarkers}\n'
154-
'Actual:\n${actual.textWithSelectionMarkers}');
155-
}
156-
157-
expect(actual.selectionStart, equals(formatTest.output.selectionStart));
158-
expect(
159-
actual.selectionLength, equals(formatTest.output.selectionLength));
137+
var actual = _validateFormat(
138+
formatter,
139+
formatTest.input,
140+
formatTest.output,
141+
'did not match expectation',
142+
testFile.isCompilationUnit);
143+
144+
// Make sure that formatting is idempotent. Format the output and make
145+
// sure we get the same result.
146+
_validateFormat(formatter, actual, formatTest.output,
147+
'was not idempotent', testFile.isCompilationUnit);
160148
});
161149
}
162150
});
163151
}
164152

153+
/// Run [formatter] on [input] and validate that the result matches [expected].
154+
///
155+
/// If not, fails with an error using [reason].
156+
///
157+
/// Returns the formatted output.
158+
SourceCode _validateFormat(DartFormatter formatter, SourceCode input,
159+
SourceCode expected, String reason, bool isCompilationUnit) {
160+
var actual = formatter.formatSource(input);
161+
162+
// The test files always put a newline at the end of the expectation.
163+
// Statements from the formatter (correctly) don't have that, so add
164+
// one to line up with the expected result.
165+
var actualText = actual.text;
166+
if (!isCompilationUnit) actualText += '\n';
167+
168+
// Fail with an explicit message because it's easier to read than
169+
// the matcher output.
170+
if (actualText != expected.text) {
171+
fail('Formatting $reason. Expected:\n'
172+
'${expected.text}\nActual:\n$actualText');
173+
} else if (actual.selectionStart != expected.selectionStart ||
174+
actual.selectionLength != expected.selectionLength) {
175+
fail('Selection $reason. Expected:\n'
176+
'${expected.textWithSelectionMarkers}\n'
177+
'Actual:\n${actual.textWithSelectionMarkers}');
178+
}
179+
180+
return actual;
181+
}
182+
165183
/// Create a test `.dart_tool` directory with a package config for a package
166184
/// with [rootPackageName] and language version [major].[minor].
167185
///

0 commit comments

Comments
 (0)