blob: 174a551eba40a2e6773dcfea201c128a8488ccee [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 '../profile.dart';
typedef Constrain = void Function(Piece other, State constrainedState);
/// Base class for the formatter's internal representation used for line
/// splitting.
///
/// We visit the source AST and convert it to a tree of [Piece]s. This tree
/// roughly follows the AST but includes comments and is optimized for
/// formatting and line splitting. The final output is then determined by
/// deciding which pieces split and how.
abstract class Piece {
/// The ordered list of all possible ways this piece could split.
///
/// Piece subclasses should override this if they support being split in
/// multiple different ways.
///
/// Each piece determines what each [State] in the list represents, including
/// the automatically included [State.unsplit]. The list returned by this
/// function should be sorted so that earlier states in the list compare less
/// than later states.
List<State> get additionalStates => const [];
/// If this piece has been pinned to a specific state, that state.
///
/// This is used when a piece which otherwise supports multiple ways of
/// splitting should be eagerly constrained to a specific splitting choice
/// because of the context where it appears. For example, if conditional
/// expressions are nested, then all of them are forced to split because it's
/// too hard to read nested conditionals all on one line. We can express that
/// by pinning the Piece used for a conditional expression to its split state
/// when surrounded by or containing other conditionals.
State? get pinnedState => _pinnedState;
State? _pinnedState;
/// Whether this piece or any of its children contain an explicit mandatory
/// newline.
///
/// This is lazily computed and cached for performance, so should only be
/// accessed after all of the piece's children are known.
late final bool containsNewline = _calculateContainsNewline();
bool _calculateContainsNewline() {
var anyHasNewline = false;
forEachChild((child) {
anyHasNewline |= child.containsNewline;
});
return anyHasNewline;
}
/// The total number of characters of content in this piece and all of its
/// children.
///
/// This is lazily computed and cached for performance, so should only be
/// accessed after all of the piece's children are known.
late final int totalCharacters = _calculateTotalCharacters();
int _calculateTotalCharacters() {
var total = 0;
forEachChild((child) {
total += child.totalCharacters;
});
return total;
}
Piece() {
Profile.count('create Piece');
}
/// Apply any constraints that this piece places on other pieces when this
/// piece is bound to [state].
///
/// A piece class can override this. For any child piece that it wants to
/// constrain when this piece is in [state], call [constrain] and pass in the
/// child piece and the state that child should be constrained to.
void applyConstraints(State state, Constrain constrain) {}
/// Given that this piece is in [state], use [writer] to produce its formatted
/// output.
void format(CodeWriter writer, State state);
/// Invokes [callback] on each piece contained in this piece.
void forEachChild(void Function(Piece piece) callback);
/// If the piece can determine that it will always end up in a certain state
/// given [pageWidth] and size metrics returned by calling [containsNewline]
/// and [totalCharacters] on its children, then returns that [State].
///
/// For example, a series of infix operators wider than a page will always
/// split one per operator. If we can determine this eagerly just based on
/// the size of the children and the page width, then we can pin the Piece to
/// that State. That in turn heavily prunes the search space that the [Solver]
/// is exploring. In practice, for large expressions, many of the outermost
/// Pieces can be eagerly pinned to their fully split state. That avoids the
/// Solver wasting a lot of time trying in vain to pack those outer Pieces
/// into unsplit states when it's obvious just from the size of their contents
/// that they'll have to split.
///
/// If it's not possible to determine whether a piece will split from its
/// metrics, this returns `null`.
///
/// This is purely an optimization: Running the [Solver] without ever calling
/// this and pinning the resulting [State] should yield the same formatting.
/// It is up to the [Piece] subclasses overriding this to ensure that they
/// only return a non-`null` [State] if the piece really would always be
/// solved to the returned state given its children.
State? fixedStateForPageWidth(int pageWidth) => null;
/// The cost that this piece should apply to the solution when in [state].
///
/// This is usually just the state's cost, but some pieces may want to tweak
/// the cost in certain circumstances.
// TODO(tall): Given that we have this API now, consider whether it makes
// sense to remove the cost field from State entirely.
int stateCost(State state) => state.cost;
/// Forces this piece to always use [state].
void pin(State state) {
// Only pin a piece once. This can happen if the contents of an
// interpolation expression (which are pinned to prevent splitting) are
// large enough for [fixedStateForPageWidth()] to also try to pin it to its
// fully split state.
if (_pinnedState != null) return;
_pinnedState = state;
// If this piece's pinned state constrains any child pieces, pin those too,
// recursively.
applyConstraints(state, (other, constrainedState) {
other.pin(constrainedState);
});
}
/// Pin the piece to whatever state is needed to prevent it from splitting.
void preventSplit() {
// For most pieces, the initial state does it.
pin(State.unsplit);
}
/// The name of this piece as it appears in debug output.
///
/// By default, this is the class's name with `Piece` removed.
String get debugName => runtimeType.toString().replaceAll('Piece', '');
@override
String toString() => '$debugName${_pinnedState ?? ''}';
}
/// A simple atomic piece of code.
///
/// This may represent a series of tokens where no split can occur between them.
/// It may also contain one or more comments.
sealed class TextPiece extends Piece {
/// RegExp that matches any valid Dart line terminator.
static final _lineTerminatorPattern = RegExp(r'\r\n?|\n');
/// The lines of text in this piece.
///
/// Most [TextPieces] will contain only a single line, but a piece for a
/// multi-line string or comment will have multiple lines. These are stored
/// as separate lines instead of a single multi-line Dart String so that
/// line endings are normalized and so that column calculation during line
/// splitting calculates each line in the piece separately.
final List<String> _lines = [''];
/// The offset from the beginning of [text] where the selection starts, or
/// `null` if the selection does not start within this chunk.
int? _selectionStart;
/// The offset from the beginning of [text] where the selection ends, or
/// `null` if the selection does not start within this chunk.
int? _selectionEnd;
/// Append [text] to the end of this piece.
///
/// If [text] may contain any newline characters, then [multiline] must be
/// `true`.
///
/// If [selectionStart] and/or [selectionEnd] are given, then notes that the
/// corresponding selection markers appear that many code units from where
/// [text] will be appended.
void append(String text,
{bool multiline = false, int? selectionStart, int? selectionEnd}) {
if (selectionStart != null) {
_selectionStart = _adjustSelection(selectionStart);
}
if (selectionEnd != null) {
_selectionEnd = _adjustSelection(selectionEnd);
}
if (multiline) {
var lines = text.split(_lineTerminatorPattern);
for (var i = 0; i < lines.length; i++) {
if (i > 0) _lines.add('');
_lines.last += lines[i];
}
} else {
_lines.last += text;
}
}
/// Sets [selectionStart] to be [start] code units after the end of the
/// current text in this piece.
void startSelection(int start) {
_selectionStart = _adjustSelection(start);
}
/// Sets [selectionEnd] to be [end] code units after the end of the
/// current text in this piece.
void endSelection(int end) {
_selectionEnd = _adjustSelection(end);
}
/// Adjust [offset] by the current length of this [TextPiece].
int _adjustSelection(int offset) {
for (var line in _lines) {
offset += line.length;
}
return offset;
}
void _formatSelection(CodeWriter writer) {
if (_selectionStart case var start?) {
writer.startSelection(start);
}
if (_selectionEnd case var end?) {
writer.endSelection(end);
}
}
void _formatLines(CodeWriter writer) {
for (var i = 0; i < _lines.length; i++) {
if (i > 0) writer.newline(flushLeft: i > 0);
writer.write(_lines[i]);
}
}
@override
bool _calculateContainsNewline() => _lines.length > 1;
@override
int _calculateTotalCharacters() {
var total = 0;
for (var line in _lines) {
total += line.length;
}
return total;
}
@override
String toString() => '`${_lines.join('¬')}`';
}
/// [TextPiece] for non-comment source code that may have comments attached to
/// it.
class CodePiece extends TextPiece {
/// Pieces for any comments that appear immediately before this code.
final List<Piece> _leadingComments;
/// Pieces for any comments that hang off the same line as this code.
final List<Piece> _hangingComments = [];
CodePiece([this._leadingComments = const []]);
void addHangingComment(Piece comment) {
_hangingComments.add(comment);
}
@override
void format(CodeWriter writer, State state) {
_formatSelection(writer);
if (_leadingComments.isNotEmpty) {
// Always put leading comments on a new line.
writer.newline();
for (var comment in _leadingComments) {
writer.format(comment);
}
}
_formatLines(writer);
for (var comment in _hangingComments) {
writer.space();
writer.format(comment);
}
}
@override
void forEachChild(void Function(Piece piece) callback) {
_leadingComments.forEach(callback);
_hangingComments.forEach(callback);
}
}
/// A [TextPiece] for a source code comment and the whitespace after it, if any.
class CommentPiece extends TextPiece {
/// Whitespace at the end of the comment.
final Whitespace _trailingWhitespace;
CommentPiece([this._trailingWhitespace = Whitespace.none]);
@override
void format(CodeWriter writer, State state) {
_formatSelection(writer);
_formatLines(writer);
writer.whitespace(_trailingWhitespace);
}
@override
bool _calculateContainsNewline() =>
_trailingWhitespace.hasNewline || super._calculateContainsNewline();
@override
void forEachChild(void Function(Piece piece) callback) {}
}
/// A piece that writes a single space.
class SpacePiece extends Piece {
@override
void forEachChild(void Function(Piece piece) callback) {}
@override
void format(CodeWriter writer, State state) {
writer.space();
}
@override
bool _calculateContainsNewline() => false;
@override
int _calculateTotalCharacters() => 1;
}
/// A state that a piece can be in.
///
/// Each state identifies one way that a piece can be split into multiple lines.
/// Each piece determines how its states are interpreted.
class State implements Comparable<State> {
static const unsplit = State(0, cost: 0);
/// The maximally split state a piece can be in.
///
/// The value here is somewhat arbitrary. It just needs to be larger than
/// any other value used by any [Piece] that uses this [State].
static const split = State(255);
final int _value;
/// How much a solution is penalized when this state is chosen.
final int cost;
const State(this._value, {this.cost = 1});
@override
int compareTo(State other) => _value.compareTo(other._value);
@override
String toString() => '◦$_value';
}