| // 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}); |
| } |