blob: c815cd4dc037a9efc2d33204b969b9a091beeebc [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 '../back_end/code_writer.dart';
import '../constants.dart';
import 'piece.dart';
/// A piece for a splittable series of items.
///
/// Items may optionally be delimited with brackets and may have commas added
/// after elements.
///
/// Used for argument lists, collection literals, parameter lists, etc. This
/// class handles adding and removing the trailing comma depending on whether
/// the list is split or not. It handles comments inside the sequence of
/// elements.
///
/// These pieces can be formatted in one of three ways:
///
/// [State.split] Fully unsplit:
///
/// function(argument, argument, argument);
///
/// If one of the elements is a "block element", then we allow newlines inside
/// it to support output like:
///
/// function(argument, () {
/// blockElement;
/// }, argument);
///
/// [_splitState] Split around all of the items:
///
/// function(
/// argument,
/// argument,
/// argument,
/// );
///
/// ListPieces are usually constructed using [createList()] or
/// [DelimitedListBuilder].
class ListPiece extends Piece {
/// The opening bracket before the elements, if any.
final Piece? _before;
/// The list of elements.
final List<ListElement> _elements;
/// The elements that should have a blank line preserved between them and the
/// next piece.
final Set<ListElement> _blanksAfter;
/// The closing bracket after the elements, if any.
final Piece? _after;
/// The details of how this particular list should be formatted.
final ListStyle _style;
ListPiece(this._before, this._elements, this._blanksAfter, this._after,
this._style);
@override
List<State> get additionalStates => [if (_elements.isNotEmpty) State.split];
@override
int stateCost(State state) {
if (state == State.split) return _style.splitCost;
return super.stateCost(state);
}
@override
void format(CodeWriter writer, State state) {
// Format the opening bracket, if there is one.
if (_before case var before?) {
if (_style.splitListIfBeforeSplits && state == State.unsplit) {
writer.setAllowNewlines(false);
}
writer.format(before);
if (state == State.unsplit) writer.setAllowNewlines(false);
// Whitespace after the opening bracket.
writer.splitIf(state != State.unsplit,
indent: Indent.block,
space: _style.spaceWhenUnsplit && _elements.isNotEmpty);
}
// Format the elements.
for (var i = 0; i < _elements.length; i++) {
var isLast = i == _elements.length - 1;
var appendComma = switch (_style.commas) {
// Has a comma after every element.
Commas.alwaysTrailing => true,
// Trailing comma after the last element if split but not otherwise.
Commas.trailing => !(state == State.unsplit && isLast),
// Never a trailing comma after the last element.
Commas.nonTrailing => !isLast,
Commas.none => false,
};
var element = _elements[i];
// Only some elements (usually a single block element) allow newlines
// when the list itself isn't split.
writer.setAllowNewlines(element.allowNewlines || state == State.split);
// If this element allows newlines when the list isn't split, add
// indentation if it requires it.
if (state == State.unsplit && element.indentWhenBlockFormatted) {
writer.setIndent(Indent.expression);
}
element.format(writer,
appendComma: appendComma,
// Only allow newlines in comments if we're fully split.
allowNewlinesInComments: state == State.split);
if (state == State.unsplit && element.indentWhenBlockFormatted) {
writer.setIndent(Indent.none);
}
// Write a space or newline between elements.
if (!isLast) {
writer.splitIf(state != State.unsplit,
blank: _blanksAfter.contains(element),
// No space after the "[" or "{" in a parameter list.
space: element._delimiter.isEmpty);
}
}
// Format the closing bracket, if any.
if (_after case var after?) {
// Whitespace before the closing bracket.
writer.splitIf(state != State.unsplit,
indent: Indent.none,
space: _style.spaceWhenUnsplit && _elements.isNotEmpty);
writer.setAllowNewlines(true);
writer.format(after);
}
}
@override
void forEachChild(void Function(Piece piece) callback) {
if (_before case var before?) callback(before);
for (var argument in _elements) {
argument.forEachChild(callback);
}
if (_after case var after?) callback(after);
}
}
/// An element in a [ListPiece].
///
/// Contains a piece for the element itself and a comment. Both are optional,
/// but at least one must be present. A [ListElement] containing only a comment
/// is used when a comment appears in a place where it gets formatted like a
/// standalone element. A [ListElement] containing both an element piece and a
/// comment piece represents an element with a hanging comment after the
/// (potentially ommitted) comma:
///
/// function(
/// first,
/// // Standalone.
/// second, // Hanging.
///
/// Here, `first` is a [ListElement] with only an element, `// Standalone.` is
/// a [ListElement] with only a comment, and `second, // Hanging.` is a
/// [ListElement] with both where `second` is the element and `// Hanging` is
/// the comment.
final class ListElement {
/// The leading inline block comments before the content.
final List<Piece> _leadingComments;
final Piece? _content;
/// What kind of block formatting can be applied to this element.
final BlockFormat blockFormat;
/// Whether newlines are allowed in this element when this list is unsplit.
///
/// This is generally only true for a single "block" element, as in:
///
/// function(argument, [
/// block,
/// element,
/// ], another);
bool allowNewlines = false;
/// Whether we should increase indentation when formatting this element when
/// the list isn't split.
///
/// This only comes into play for unsplit lists and is only relevant when the
/// element contains newlines, which means that this is only ever useful when
/// [allowNewlines] is also true.
///
/// This is used for adjacent strings expression at the beginning of an
/// argument list followed by a function expression, like in a `test()` call.
/// Since the adjacent strings may not require indentation when the list is
/// fully split, this ensures that they are indented properly when the list
/// isn't split. Avoids:
//
// test('long description'
// 'that should be indented', () {
// body;
// });
bool indentWhenBlockFormatted = false;
/// If this piece has an opening delimiter after the comma, this is its
/// lexeme, otherwise an empty string.
///
/// This is only used for parameter lists when an optional or named parameter
/// section begins in the middle of the parameter list, like:
///
/// function(
/// int parameter1, [
/// int parameter2,
/// ]);
String _delimiter = '';
/// The hanging inline block and line comments that appear after the content.
final List<Piece> _hangingComments = [];
/// The number of hanging comments that should appear before the delimiter.
///
/// A list item may have hanging comments before and after the delimiter, as
/// in:
///
/// function(
/// argument /* 1 */ /* 2 */, /* 3 */ /* 4 */ // 5
/// );
///
/// This field counts the number of comments that should be before the
/// delimiter (here `,` and 2).
int _commentsBeforeDelimiter = 0;
ListElement(List<Piece> leadingComments, Piece element, BlockFormat format)
: _leadingComments = [...leadingComments],
_content = element,
blockFormat = format;
ListElement.comment(Piece comment)
: _leadingComments = const [],
_content = null,
blockFormat = BlockFormat.none {
_hangingComments.add(comment);
}
void addComment(Piece comment, {bool beforeDelimiter = false}) {
_hangingComments.add(comment);
if (beforeDelimiter) _commentsBeforeDelimiter++;
}
void setDelimiter(String delimiter) {
_delimiter = delimiter;
}
void format(CodeWriter writer,
{required bool appendComma, required bool allowNewlinesInComments}) {
for (var comment in _leadingComments) {
writer.format(comment);
writer.space();
}
if (_content case var content?) {
writer.format(content);
for (var i = 0; i < _commentsBeforeDelimiter; i++) {
writer.space();
writer.format(_hangingComments[i]);
}
if (appendComma) writer.write(',');
if (_delimiter.isNotEmpty) {
writer.space();
writer.write(_delimiter);
}
}
writer.setAllowNewlines(allowNewlinesInComments);
for (var i = _commentsBeforeDelimiter; i < _hangingComments.length; i++) {
if (i > 0 || _content != null) writer.space();
writer.format(_hangingComments[i]);
}
}
void forEachChild(void Function(Piece piece) callback) {
_leadingComments.forEach(callback);
if (_content case var content?) callback(content);
_hangingComments.forEach(callback);
}
}
/// Where commas should be added in a [ListPiece].
enum Commas {
/// Add a comma after every element, regardless of whether or not it is split.
alwaysTrailing,
/// Add a comma after every element when the elements split, including the
/// last. When not split, omit the trailing comma.
trailing,
/// Add a comma after every element except for the last, regardless of whether
/// or not it is split.
nonTrailing,
/// Don't add commas after any elements.
none,
}
/// What kind of block formatting style can be applied to the element.
enum BlockFormat {
/// The element is a function expression, which takes priority over other
/// kinds of block formatted elements.
function,
/// The element is a collection literal or some other kind expression that
/// can be block formatted.
block,
/// The element is an adjacent strings expression that's in an list that
/// requires its subsequent lines to be indented (because there are other
/// string literal in the list).
indentedAdjacentStrings,
/// The element is an adjacent strings expression that's in an list that
/// doesn't require its subsequent lines to be indented (because there
/// are no other string literals in the list).
unindentedAdjacentStrings,
/// The element can't be block formatted.
none,
}
/// The various ways a "list" can appear syntactically and be formatted.
///
/// [ListPiece] is used for most places in code where a series of elements can
/// be either all on one line or can be each split to their own line with no
/// extra indentation: argument lists, parameter lists, collection literals,
/// type arguments, switch expression cases, etc.
///
/// These have similar enough formatting to use the same class. And, in
/// particular, they all handle comments between elements the same way. But
/// they vary in whether or not a trailing comma is allowed, whether there
/// should be spaces inside the delimiters when the elements aren't split, etc.
/// This class captures those options.
class ListStyle {
/// How commas should be handled by the list.
///
/// Most lists use [Commas.trailing]. Type parameters and type arguments use
/// [Commas.nonTrailing]. For loop parts and switch values use [Commas.none].
final Commas commas;
/// The cost of splitting this list. Normally 1, but higher for some lists
/// that look worse when split.
final int splitCost;
/// Whether this list should have spaces inside the bracket when it doesn't
/// split. This is false for most lists, but true for switch expression
/// bodies:
///
/// v = switch (e) { 1 => 'one', 2 => 'two' };
/// // ^ ^
final bool spaceWhenUnsplit;
/// Whether a split in the [_before] piece should force the list to split too.
/// Most of the time, this isn't relevant because the before part is usually
/// just a single bracket character.
///
/// For collection literals with explicit type arguments, the [_before] piece
/// contains the type arguments. If those split, this is `false` to allow the
/// list itself to remain unsplit as in:
///
/// <
/// VeryLongTypeName,
/// AnotherLongTypeName,
/// >{a: 1};
///
/// For switch expressions, the `switch (value) {` part is in [_before] and
/// the body is the list. In that case, if the value splits, we want to force
/// the body to split too:
///
/// // Disallowed:
/// e = switch (
/// "a long string that must wrap"
/// ) { 0 => "ok" };
///
/// // Instead:
/// e = switch (
/// "a long string that must wrap"
/// ) {
/// 0 => "ok",
/// };
final bool splitListIfBeforeSplits;
/// Whether an element in the list is allowed to have block-like formatting,
/// as in:
///
/// function(argument, [
/// block,
/// like,
/// ], argument);
final bool allowBlockElement;
const ListStyle(
{this.commas = Commas.trailing,
this.splitCost = Cost.normal,
this.spaceWhenUnsplit = false,
this.splitListIfBeforeSplits = false,
this.allowBlockElement = false});
}