blob: dd7546f8a407042229b7c0d3ffaa1367d7469867 [file] [edit]
// 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 '../ast_extensions.dart';
import '../back_end/code_writer.dart';
import '../piece/piece.dart';
import '../piece/sequence.dart';
import '../piece/text.dart';
import 'piece_factory.dart';
/// Incrementally builds a [SequencePiece], including handling comments and
/// newlines that may appear before, between, or after its contents.
///
/// Comments are handled specially here so that we can give them better
/// formatting than we would be able to if we treated all comments generally.
///
/// Most comments appear around statements in a block, members in a class, or
/// at the top level of a file. For those, we treat them essentially like
/// separate statements inside the sequence. This lets us gracefully handle
/// indenting them and supporting blank lines around them the same way we handle
/// other statements or members in a sequence.
final class SequenceBuilder {
final PieceFactory _visitor;
/// The opening bracket before the elements, if any.
Piece? _leftBracket;
/// The series of elements in the sequence.
final List<SequenceElementPiece> _elements = [];
/// The closing bracket after the elements, if any.
Piece? _rightBracket;
/// Whether a blank line should be allowed after the current element.
bool _allowBlank = false;
SequenceBuilder(this._visitor);
Piece build({bool forceSplit = false}) {
// If the sequence only contains a single piece, just return it directly
// and discard the unnecessary wrapping.
if (_leftBracket == null &&
_elements.length == 1 &&
_elements.single.hangingComments.isEmpty &&
_rightBracket == null) {
return _elements.single.piece;
}
// If there are no elements, don't bother making a SequencePiece or
// BlockPiece.
if (_elements.isEmpty) {
return _visitor.pieces.build(() {
if (_leftBracket case var bracket?) _visitor.pieces.add(bracket);
if (forceSplit || _leftBracket == null) {
_visitor.pieces.add(NewlinePiece());
}
if (_rightBracket case var bracket?) _visitor.pieces.add(bracket);
});
}
// Discard any trailing blank line after the last element.
_elements.last.blankAfter = false;
var sequence = SequencePiece(_elements);
if ((_leftBracket, _rightBracket) case (var left?, var right?)) {
return BlockPiece(left, sequence, right);
}
return sequence;
}
/// Adds the opening [bracket] to the built sequence.
void leftBracket(Token bracket, {bool soft = false}) {
_leftBracket = _visitor.tokenPiece(bracket, soft: soft);
}
/// Adds the closing [bracket] to the built sequence along with any comments
/// that precede it.
void rightBracket(Token bracket) {
// Place any comments before the bracket inside the block.
addCommentsBefore(bracket);
_rightBracket = _visitor.tokenPiece(bracket);
}
/// Adds [piece] to this sequence.
///
/// The caller should have already called [addCommentsBefore()] with the
/// first token in [piece].
void add(Piece piece, {Indent? indent, bool allowBlankAfter = true}) {
_elements.add(SequenceElementPiece(indent ?? Indent.none, piece));
_allowBlank = allowBlankAfter;
}
/// Visits [node] and adds the resulting [Piece] to this sequence, handling
/// any comments or blank lines that appear before it.
///
/// If [blankBefore] is `true`, then ensures there is a blank line before
/// [token]. If there any blank lines around the comments, then the first one
/// will be preserved. Otherwise, writes a blank line before the first
/// comment.
void visit(AstNode node, {Indent? indent, bool blankBefore = false}) {
var token = node.firstNonCommentToken;
var wroteBlank = addCommentsBefore(
token,
indent: indent,
blankBefore: blankBefore,
);
if (blankBefore && !wroteBlank) addBlank();
add(_visitor.nodePiece(node), indent: indent);
}
/// Appends a blank line before the next piece in the sequence.
void addBlank() {
if (_elements.isEmpty) return;
if (!_allowBlank) return;
_elements.last.blankAfter = true;
}
/// Writes any comments appearing before [token] to the sequence.
///
/// Also handles blank lines between preceding comments and elements and the
/// subsequent element.
///
/// Comments between sequence elements get special handling where comments
/// on their own line become standalone sequence elements.
///
/// If [blankBefore] is `true`, then ensures there is a blank line before
/// [token]. If there any blank lines around the comments, then the first one
/// will be preserved. Otherwise, writes a blank line before the first
/// comment.
///
/// Returns `true` if processing the comments ended up writing any blank
/// lines.
bool addCommentsBefore(
Token token, {
Indent? indent,
bool blankBefore = false,
}) {
indent ??= Indent.none;
var wroteBlank = false;
var comments = _visitor.comments.takeCommentsBefore(token);
// Default to putting the blank line before all comments unless there are
// blank lines inside or after them.
if (blankBefore && !comments.containsBlankLine) {
addBlank();
wroteBlank = true;
}
for (var i = 0; i < comments.length; i++) {
var comment = _visitor.pieces.commentPiece(comments[i]);
if (_elements.isNotEmpty && comments.isHanging(i)) {
// Attach the comment to the previous element.
_elements.last.hangingComments.add(comment);
} else {
if (comments.linesBefore(i) > 1) {
// Always preserve a blank line above sequence-level comments.
_allowBlank = true;
addBlank();
wroteBlank = true;
}
// Write the comment as its own sequence piece.
_elements.add(SequenceElementPiece(indent, comment));
}
}
// Write a blank before the token if there should be one.
if (comments.linesBeforeNextToken > 1) {
// If we just wrote a comment, then allow a blank line between it and the
// element.
if (comments.isNotEmpty) _allowBlank = true;
addBlank();
wroteBlank = true;
}
return wroteBlank;
}
}