Skip to content

Commit 967d15e

Browse files
authored
Ensure that line comments always get newlines after them. (#1325)
Ensure that line comments always get newlines after them. The formatter currently mostly relies on the surrounding piece's setAllowedWhitespace() rules to make sure that a line comment forces the surrounding piece to split in a way that ensures there is a newline after the comment. This works correctly in lots of places where line comments are actually expected, but isn't reliable when a line comment occurs in a weird location. When that happens, it's important that we still always put a newline after the comment. Otherwise the code is meaningfully changed. This does that. Whenever a TextPiece ends in a line comment, it will know that it has a trailing newline. It then always writes that to the CodeWriter. That leads to redundant newlines in cases where the surrounding piece is already going to put a newline, so I also moved some of the "pending" newline collapsing code from PieceWriter farther down the pipeline to CodeWriter. This doesn't change the behavior of any of the current tests. It will fix many untested corners of the language. I'm working on some new tests for comments in if statements that will be fixed by this change, but I'm also changing the actual formatting of if statements, so I want to do that in a separate change.
1 parent 28fa4be commit 967d15e

File tree

4 files changed

+98
-26
lines changed

4 files changed

+98
-26
lines changed

lib/src/back_end/code_writer.dart

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ class CodeWriter {
2828
/// Buffer for the code being written.
2929
final StringBuffer _buffer = StringBuffer();
3030

31+
/// What whitespace should be written before the next non-whitespace text.
32+
///
33+
/// When whitespace is written, instead of immediately writing it, we queue
34+
/// it as pending. This ensures that we don't write trailing whitespace,
35+
/// avoids writing spaces at the beginning of lines, and allows collapsing
36+
/// multiple redundant newlines.
37+
_Whitespace _pendingWhitespace = _Whitespace.none;
38+
39+
/// The number of spaces of indentation that should be begin the next line
40+
/// when [_pendingWhitespace] is [_Whitespace.newline] or
41+
/// [_Whitespace.blankLine].
42+
int _pendingIndent = 0;
43+
3144
/// The cost of the currently chosen line splits.
3245
int _cost = 0;
3346

@@ -143,6 +156,7 @@ class CodeWriter {
143156
// TextPieces because that will preserve selection markers. Consider doing
144157
// something smarter for commas in lists and semicolons in for loops.
145158

159+
_flushWhitespace();
146160
_buffer.write(text);
147161
_column += text.length;
148162

@@ -180,7 +194,10 @@ class CodeWriter {
180194

181195
/// Writes a single space to the output.
182196
void space() {
183-
write(' ');
197+
// If a newline is already pending, then ignore the space.
198+
if (_pendingWhitespace == _Whitespace.none) {
199+
_pendingWhitespace = _Whitespace.space;
200+
}
184201
}
185202

186203
/// Inserts a line split in the output.
@@ -193,12 +210,15 @@ class CodeWriter {
193210
if (indent != null) setIndent(indent);
194211

195212
handleNewline();
196-
_finishLine();
197-
_buffer.writeln();
198-
if (blank) _buffer.writeln();
199213

200-
_column = _options.indent;
201-
_buffer.write(' ' * _column);
214+
// Collapse redundant newlines.
215+
if (blank) {
216+
_pendingWhitespace = _Whitespace.blankLine;
217+
} else if (_pendingWhitespace != _Whitespace.blankLine) {
218+
_pendingWhitespace = _Whitespace.newline;
219+
}
220+
221+
_pendingIndent = _options.indent;
202222
}
203223

204224
/// Sets whether newlines are allowed to occur from this point on for the
@@ -246,15 +266,46 @@ class CodeWriter {
246266
/// Sets [selectionStart] to be [start] code units into the output.
247267
void startSelection(int start) {
248268
assert(_selectionStart == null);
269+
270+
_flushWhitespace();
249271
_selectionStart = _buffer.length + start;
250272
}
251273

252274
/// Sets [selectionEnd] to be [end] code units into the output.
253275
void endSelection(int end) {
254276
assert(_selectionEnd == null);
277+
278+
_flushWhitespace();
255279
_selectionEnd = _buffer.length + end;
256280
}
257281

282+
/// Write any pending whitespace.
283+
///
284+
/// This is called before non-whitespace text is about to be written, or
285+
/// before the selection is updated since the latter requires an accurate
286+
/// count of the written text, including whitespace.
287+
void _flushWhitespace() {
288+
switch (_pendingWhitespace) {
289+
case _Whitespace.none:
290+
break; // Nothing to do.
291+
292+
case _Whitespace.newline:
293+
case _Whitespace.blankLine:
294+
_finishLine();
295+
_buffer.writeln();
296+
if (_pendingWhitespace == _Whitespace.blankLine) _buffer.writeln();
297+
298+
_column = _pendingIndent;
299+
_buffer.write(' ' * _column);
300+
301+
case _Whitespace.space:
302+
_buffer.write(' ');
303+
_column++;
304+
}
305+
306+
_pendingWhitespace = _Whitespace.none;
307+
}
308+
258309
void _finishLine() {
259310
// If the completed line is too long, track the overflow.
260311
if (_column >= _pageWidth) {
@@ -276,6 +327,21 @@ class CodeWriter {
276327
}
277328
}
278329

330+
/// Different kinds of pending whitespace that have been requested.
331+
enum _Whitespace {
332+
/// No pending whitespace.
333+
none,
334+
335+
/// A single newline.
336+
newline,
337+
338+
/// Two newlines.
339+
blankLine,
340+
341+
/// A single space.
342+
space
343+
}
344+
279345
/// The mutable state local to a single piece being formatted.
280346
class _PieceOptions {
281347
/// The absolute number of spaces of leading indentation coming from

lib/src/front_end/comment_writer.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ mixin CommentWriter {
7878

7979
if (comments.isHanging(i)) {
8080
// Attach the comment to the previous token.
81-
pieces.writeSpace();
8281
pieces.writeComment(comment, hanging: true);
8382
} else {
8483
pieces.writeNewline();
@@ -202,8 +201,7 @@ class SourceComment {
202201
{required this.flushLeft, required this.offset});
203202

204203
/// Whether this comment contains a mandatory newline, either because it's a
205-
/// comment that should be on its own line or a lexeme with a newline inside
206-
/// it (i.e. multi-line block comment, multi-line string).
204+
/// comment that should be on its own line or is a multi-line block comment.
207205
bool get containsNewline =>
208206
type != CommentType.inlineBlock || text.contains('\n');
209207

lib/src/front_end/piece_writer.dart

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,6 @@ class PieceWriter {
124124
/// Whether we should write a space before the next text that is written.
125125
bool _pendingSpace = false;
126126

127-
/// Whether we should write a newline in the current [TextPiece] before the
128-
/// next text that is written.
129-
bool _pendingNewline = false;
130-
131127
/// Whether we should create a new [TextPiece] the next time text is written.
132128
bool _pendingSplit = false;
133129

@@ -221,14 +217,14 @@ class PieceWriter {
221217

222218
/// Writes a mandatory newline from a comment to the current [TextPiece].
223219
void writeNewline() {
224-
_pendingNewline = true;
220+
_currentText!.newline();
225221
}
226222

227223
/// Write the contents of [comment] to the current innermost [TextPiece],
228224
/// handling any newlines that may appear in it.
229225
///
230226
/// If [hanging] is `true`, then the comment is appended to the current line
231-
/// even if call to [split()] has just happened. This is used for writing a
227+
/// even if a call to [split()] has happened. This is used for writing a
232228
/// comment that should be on the end of a line.
233229
void writeComment(SourceComment comment, {bool hanging = false}) {
234230
_write(comment.text,
@@ -246,10 +242,9 @@ class PieceWriter {
246242
// current text.
247243
if (textPiece == null || _pendingSplit && !hanging) {
248244
textPiece = _currentText = TextPiece();
249-
} else if (_pendingNewline) {
250-
textPiece.newline();
251-
} else if (_pendingSpace) {
252-
textPiece.append(' ');
245+
} else if (_pendingSpace || hanging) {
246+
// Always write a space before hanging comments.
247+
textPiece.appendSpace();
253248
}
254249

255250
if (offset != null) {
@@ -267,7 +262,6 @@ class PieceWriter {
267262
textPiece.append(text, containsNewline: containsNewline);
268263

269264
_pendingSpace = false;
270-
_pendingNewline = false;
271265
if (!hanging) _pendingSplit = false;
272266
}
273267

lib/src/piece/piece.dart

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,19 @@ class TextPiece extends Piece {
8888
/// multiline strings, etc.
8989
bool _containsNewline = false;
9090

91+
/// Whether this piece should have a newline written at the end of it.
92+
///
93+
/// This is true during piece construction while lines are still being
94+
/// written. It may also be true once a piece is fully complete if it ends in
95+
/// a line comment.
96+
bool _trailingNewline = false;
97+
9198
/// The offset from the beginning of [text] where the selection starts, or
9299
/// `null` if the selection does not start within this chunk.
93-
int? get selectionStart => _selectionStart;
94100
int? _selectionStart;
95101

96102
/// The offset from the beginning of [text] where the selection ends, or
97103
/// `null` if the selection does not start within this chunk.
98-
int? get selectionEnd => _selectionEnd;
99104
int? _selectionEnd;
100105

101106
/// Whether the last line of this piece's text ends with [text].
@@ -106,16 +111,25 @@ class TextPiece extends Piece {
106111
/// If [text] internally contains a newline, then [containsNewline] should
107112
/// be `true`.
108113
void append(String text, {bool containsNewline = false}) {
109-
if (_lines.isEmpty) _lines.add('');
114+
if (_lines.isEmpty || _trailingNewline) _lines.add('');
110115

111116
// TODO(perf): Consider a faster way of accumulating text.
112117
_lines.last = _lines.last + text;
113118

114119
if (containsNewline) _containsNewline = true;
120+
121+
_trailingNewline = false;
122+
}
123+
124+
void appendSpace() {
125+
// Don't write an unnecessary space at the beginning of a line.
126+
if (_trailingNewline) return;
127+
128+
append(' ');
115129
}
116130

117131
void newline() {
118-
_lines.add('');
132+
_trailingNewline = true;
119133
}
120134

121135
@override
@@ -136,6 +150,8 @@ class TextPiece extends Piece {
136150
if (i > 0) writer.newline();
137151
writer.write(_lines[i]);
138152
}
153+
154+
if (_trailingNewline) writer.newline();
139155
}
140156

141157
@override
@@ -149,7 +165,6 @@ class TextPiece extends Piece {
149165
start += line.length;
150166
}
151167

152-
// print('TextPiece start $start (absolute = $abs)');
153168
_selectionStart = start;
154169
}
155170

@@ -161,7 +176,6 @@ class TextPiece extends Piece {
161176
end += line.length;
162177
}
163178

164-
// print('TextPiece end $end (absolute = $abs)');
165179
_selectionEnd = end;
166180
}
167181

0 commit comments

Comments
 (0)