blob: 3c7cf39d1fc6286f6d78a14ffddf7cacbdb38782 [file] [log] [blame]
// 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 '../constants.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.
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(() {
_visitor.pieces.add(_leftBracket!);
if (forceSplit) {
_visitor.pieces.add(NewlinePiece());
}
_visitor.pieces.add(_rightBracket!);
});
}
// 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) {
_leftBracket = _visitor.tokenPiece(bracket);
}
/// 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, {int? 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.
void visit(AstNode node, {int? indent, bool allowBlankAfter = true}) {
addCommentsBefore(node.firstNonCommentToken, indent: indent);
add(_visitor.nodePiece(node),
indent: indent, allowBlankAfter: allowBlankAfter);
}
/// 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.
void addCommentsBefore(Token token, {int? indent}) {
indent ??= Indent.none;
var comments = _visitor.comments.takeCommentsBefore(token);
// Edge case: if we require a blank line, but there exists one between
// some of the comments, or after the last one, then we don't need to
// enforce one before the first comment. Example:
//
// library foo;
// // comment
//
// class Bar {}
//
// Normally, a blank line is required after `library`, but since there is
// one after the comment, we don't need one before it. This is mainly so
// that commented out directives stick with their preceding group.
if (comments.containsBlank && _elements.isNotEmpty) {
_elements.last.blankAfter = false;
}
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();
}
// 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();
}
}
}