Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamp the way tokens and comments are built into pieces. #1336

Merged
merged 4 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 33 additions & 24 deletions lib/src/back_end/code_writer.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:math';

import '../piece/piece.dart';
import 'solution.dart';

Expand Down Expand Up @@ -34,11 +36,11 @@ class CodeWriter {
/// it as pending. This ensures that we don't write trailing whitespace,
/// avoids writing spaces at the beginning of lines, and allows collapsing
/// multiple redundant newlines.
_Whitespace _pendingWhitespace = _Whitespace.none;
Whitespace _pendingWhitespace = Whitespace.none;

/// The number of spaces of indentation that should be begin the next line
/// when [_pendingWhitespace] is [_Whitespace.newline] or
/// [_Whitespace.blankLine].
/// when [_pendingWhitespace] is [Whitespace.newline] or
/// [Whitespace.blankLine].
int _pendingIndent = 0;

/// The cost of the currently chosen line splits.
Expand Down Expand Up @@ -194,10 +196,7 @@ class CodeWriter {

/// Writes a single space to the output.
void space() {
// If a newline is already pending, then ignore the space.
if (_pendingWhitespace == _Whitespace.none) {
_pendingWhitespace = _Whitespace.space;
}
whitespace(Whitespace.space);
}

/// Inserts a line split in the output.
Expand All @@ -209,16 +208,16 @@ class CodeWriter {
void newline({bool blank = false, int? indent}) {
if (indent != null) setIndent(indent);

handleNewline();
whitespace(blank ? Whitespace.blankLine : Whitespace.newline);
}

// Collapse redundant newlines.
if (blank) {
_pendingWhitespace = _Whitespace.blankLine;
} else if (_pendingWhitespace != _Whitespace.blankLine) {
_pendingWhitespace = _Whitespace.newline;
void whitespace(Whitespace whitespace) {
if (whitespace case Whitespace.newline || Whitespace.blankLine) {
handleNewline();
_pendingIndent = _options.indent;
}

_pendingIndent = _options.indent;
_pendingWhitespace = _pendingWhitespace.collapse(whitespace);
}

/// Sets whether newlines are allowed to occur from this point on for the
Expand Down Expand Up @@ -286,24 +285,24 @@ class CodeWriter {
/// count of the written text, including whitespace.
void _flushWhitespace() {
switch (_pendingWhitespace) {
case _Whitespace.none:
case Whitespace.none:
break; // Nothing to do.

case _Whitespace.newline:
case _Whitespace.blankLine:
case Whitespace.newline:
case Whitespace.blankLine:
_finishLine();
_buffer.writeln();
if (_pendingWhitespace == _Whitespace.blankLine) _buffer.writeln();
if (_pendingWhitespace == Whitespace.blankLine) _buffer.writeln();

_column = _pendingIndent;
_buffer.write(' ' * _column);

case _Whitespace.space:
case Whitespace.space:
_buffer.write(' ');
_column++;
}

_pendingWhitespace = _Whitespace.none;
_pendingWhitespace = Whitespace.none;
}

void _finishLine() {
Expand All @@ -328,18 +327,28 @@ class CodeWriter {
}

/// Different kinds of pending whitespace that have been requested.
enum _Whitespace {
///
/// Note that the order of values in the enum is significant: later ones have
/// more whitespace than previous ones.
enum Whitespace {
/// No pending whitespace.
none,

/// A single space.
space,

/// A single newline.
newline,

/// Two newlines.
blankLine,
blankLine;

/// A single space.
space
/// Combines two pending whitespaces and returns the result.
///
/// When two whitespaces overlap, they aren't both written: we don't want
/// two spaces or a newline followed by a space. Instead, the two whitespaces
/// are collapsed such that the largest one wins.
Whitespace collapse(Whitespace other) => values[max(index, other.index)];
}

/// The mutable state local to a single piece being formatted.
Expand Down
109 changes: 109 additions & 0 deletions lib/src/front_end/adjacent_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';

import '../piece/adjacent.dart';
import '../piece/piece.dart';
import 'piece_factory.dart';

/// Incrementally builds an [AdjacentPiece].
class AdjacentBuilder {
final PieceFactory _visitor;

/// The series of adjacent pieces.
final List<Piece> _pieces = [];

AdjacentBuilder(this._visitor);

/// Yields a new piece containing all of the pieces added to or created by
/// this builder. The caller must ensure it doesn't build an empty piece.
///
/// Also clears the builder's list of pieces so that this builder can be
/// reused to build more pieces.
Piece build() {
assert(_pieces.isNotEmpty);

var result = _flattenPieces();
_pieces.clear();

return result;
}

/// Adds [piece] to this builder.
void add(Piece piece) {
_pieces.add(piece);
}

/// Emit [token], along with any comments and formatted whitespace that comes
/// before it.
///
/// If [lexeme] is given, uses that for the token's lexeme instead of its own.
///
/// Does nothing if [token] is `null`. If [spaceBefore] is `true`, writes a
/// space before the token, likewise with [spaceAfter].
void token(Token? token,
{bool spaceBefore = false, bool spaceAfter = false, String? lexeme}) {
if (token == null) return;

if (spaceBefore) space();
add(_visitor.pieces.tokenPiece(token, lexeme: lexeme));
if (spaceAfter) space();
}

/// Writes any comments that appear before [token], which will be discarded.
///
/// Used to ensure comments before a discarded token are preserved.
void commentsBefore(Token? token) {
if (token == null) return;

var piece = _visitor.pieces.writeCommentsBefore(token);
if (piece != null) add(piece);
}

/// Writes an optional modifier that precedes other code.
void modifier(Token? keyword) {
token(keyword, spaceAfter: true);
}

/// Visits [node] if not `null` and adds the resulting [Piece] to this
/// builder.
void visit(AstNode? node,
{bool spaceBefore = false,
bool commaAfter = false,
bool spaceAfter = false}) {
if (node == null) return;

if (spaceBefore) space();
add(_visitor.nodePiece(node, commaAfter: commaAfter));
if (spaceAfter) space();
}

/// Appends a space before the previous piece and the next one.
void space() {
_pieces.add(SpacePiece());
}

/// Removes redundant [AdjacentPiece] wrappers from [_pieces].
Piece _flattenPieces() {
List<Piece> flattened = [];

void traverse(List<Piece> pieces) {
for (var piece in pieces) {
if (piece is AdjacentPiece) {
traverse(piece.pieces);
} else {
flattened.add(piece);
}
}
}

traverse(_pieces);

// If there's only one piece, don't wrap it in a pointless AdjacentPiece.
if (flattened.length == 1) return flattened[0];

return AdjacentPiece(flattened);
}
}
Loading