blob: 8eb92fa1828ea840229f864508e1a660c2579464 [file] [log] [blame] [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/adjacent.dart';
import '../piece/assign.dart';
import '../piece/assign_3_dot_7.dart';
import '../piece/clause.dart';
import '../piece/control_flow.dart';
import '../piece/for.dart';
import '../piece/grouping.dart';
import '../piece/if_case.dart';
import '../piece/infix.dart';
import '../piece/list.dart';
import '../piece/piece.dart';
import '../piece/prefix.dart';
import '../piece/sequence.dart';
import '../piece/type.dart';
import '../piece/variable.dart';
import 'chain_builder.dart';
import 'comment_writer.dart';
import 'delimited_list_builder.dart';
import 'expression_contents.dart';
import 'formatting_style.dart';
import 'piece_writer.dart';
import 'sequence_builder.dart';
/// Record type for a destructured binary operator-like syntactic construct.
typedef BinaryOperation = (AstNode left, Token operator, AstNode right);
/// The kind of syntax surrounding a node when being converted to a [Piece], if
/// that surrounding syntax may affect how the child node is formatted.
///
/// For example, binary operators indent their subsequent operands in most
/// places:
///
/// function(
/// operand +
/// operand,
/// );
///
/// But not when they appear on the right-hand side of an assignment or
/// assignment-like structure:
///
/// variable =
/// operand +
/// operand;
///
/// To handle this, when the code for a node recursively visits a child, it can
/// pass in a context describing itself, which the child can then access to
/// decide how it should be formatted.
enum NodeContext {
/// No specified context.
none,
/// The child is the right-hand side of an assignment-like form.
///
/// This includes assignments, variable declarations, named arguments, map
/// entries, and `=>` function bodies.
assignment,
/// The child is the target of a cascade expression.
cascadeTarget,
/// The child is the then or else operand of a conditional expression.
conditionalBranch,
/// The child is a variable declaration in a for loop.
forLoopVariable,
/// The child is an expression in a named argument or named record field.
namedExpression,
/// The child is a string interpolation inside a multiline string.
multilineStringInterpolation,
/// The child is the outermost pattern in a switch expression case.
switchExpressionCase,
}
/// Utility methods for creating pieces that share formatting logic across
/// multiple parts of the language.
///
/// Many AST nodes are structurally similar and receive similar formatting. For
/// example, imports and exports are mostly the same, with exports a subset of
/// imports. Likewise, assert statements are formatted like function calls and
/// argument lists.
///
/// This mixin defines functions that represent a general construct that is
/// formatted a certain way. The function builds up an appropriate set of
/// [Piece]s given the various AST subcomponents passed in as parameters. The
/// main [AstNodeVisitor] class then calls those for all of the AST nodes that
/// should receive that similar formatting.
///
/// These are all void functions because they generally push their result into
/// the [PieceWriter].
///
/// Naming these functions can be hard. For example, there isn't an obvious
/// word for "import or export directive" or "named thing with argument list".
/// To avoid that, we pick one concrete construct formatted by the function,
/// usually the most common, and name it after that, as in [createImport()].
mixin PieceFactory {
final ExpressionContents _contents = ExpressionContents();
FormattingStyle get style;
PieceWriter get pieces;
CommentWriter get comments;
NodeContext get parentContext;
void visitNode(AstNode node, NodeContext context);
/// Writes a [ListPiece] for an argument list.
void writeArgumentList(ArgumentList argumentList) {
writeArguments(
argumentList.leftParenthesis,
argumentList.arguments,
argumentList.rightParenthesis,
);
}
/// Writes a [ListPiece] for an argument list.
void writeArguments(
Token leftBracket,
List<Expression> arguments,
Token rightBracket,
) {
// In 3.7, we don't support preserving trailing commas or eager splitting.
if (style.is3Dot7) {
writeList(
leftBracket: leftBracket,
arguments,
rightBracket: rightBracket,
allowBlockArgument: true,
);
return;
}
// If the argument list is completely empty, write the brackets inline so
// we create fewer pieces.
if (!arguments.canSplit(rightBracket)) {
pieces.token(leftBracket);
pieces.token(rightBracket);
return;
}
_contents.beginCall(arguments);
var builder = DelimitedListBuilder(this);
builder.leftBracket(leftBracket);
builder.visitAll(arguments, allowBlockArgument: true);
builder.rightBracket(rightBracket);
var argumentsPiece = builder.build(
forceSplit: style.preserveTrailingCommaBefore(rightBracket),
);
// If the call is complex enough, force it to split even if it would fit.
if (_contents.endCall(arguments)) {
// Don't force an argument list to fully split if it could block split.
// TODO(rnystrom): Ideally, if the argument list has a block argument, we
// would force it to either block split or fully split, but disallow it
// from being all on one line. Unfortunately, the solver can't currently
// represent a constraint like that.
//
// (In fact, ListPiece doesn't even have a way to force itself to be block
// split. Instead, it simply allows newlines inside the block elements
// and whether they actually split or not is up to them.)
//
// We definitely don't want to eagerly force the argument list to fully
// split. Most of the time, that argument list will block format, and
// that will look much better than being fully split.
//
// Since we can't prevent a list piece with block arguments from being on
// one line, we don't try to eagerly split it at all. In practice, almost
// all argument lists that can block split contain either function
// expressions or large collections which will force the call to (block)
// split, so the goal of not packing too much on one line is still met
// even without this eager splitting heuristic.
if (argumentsPiece is! ListPiece || !argumentsPiece.hasBlockElement) {
argumentsPiece.pin(State.split);
}
}
pieces.add(argumentsPiece);
}
/// Writes a bracket-delimited block or declaration body.
///
/// If [forceSplit] is `true`, then the block will split even if empty. This
/// is used, for example, with empty blocks in `if` statements followed by
/// `else` clauses:
///
/// if (condition) {
/// } else {}
void writeBody(
Token leftBracket,
List<AstNode> contents,
Token rightBracket, {
bool forceSplit = false,
}) {
// If the body is completely empty, write the brackets directly inline so
// that we create fewer pieces.
if (!forceSplit && !contents.canSplit(rightBracket)) {
pieces.token(leftBracket);
pieces.token(rightBracket);
return;
}
var sequence = SequenceBuilder(this);
sequence.leftBracket(leftBracket);
for (var node in contents) {
sequence.visit(node);
// If the node has a non-empty braced body, then require a blank line
// between it and the next node.
if (node.hasNonEmptyBody) sequence.addBlank();
}
sequence.rightBracket(rightBracket);
pieces.add(sequence.build(forceSplit: forceSplit));
}
/// Writes a [SequencePiece] for a given [Block].
///
/// If [forceSplit] is `true`, then the block will split even if empty. This
/// is used, for example, with empty blocks in `if` statements followed by
/// `else` clauses:
///
/// if (condition) {
/// } else {}
void writeBlock(Block block, {bool forceSplit = false}) {
writeBody(
block.leftBracket,
block.statements,
block.rightBracket,
forceSplit: forceSplit,
);
}
/// Writes a piece for a `break` or `continue` statement.
void writeBreak(Token keyword, SimpleIdentifier? label, Token semicolon) {
pieces.token(keyword);
pieces.visit(label, spaceBefore: true);
pieces.token(semicolon);
}
void writeChain(Expression node) {
pieces.add(
ChainBuilder(
this,
node,
).build(isCascadeTarget: parentContext == NodeContext.cascadeTarget),
);
}
/// Writes a [ListPiece] for a collection literal or pattern.
///
/// If [splitEagerly] is `true`, then this collection is forced to split if
/// its contents are sufficiently complex enough to be hard to read on one
/// line even if it would otherwise fit. For example:
///
/// // Prefer:
/// data = {
/// 'a': [1, 2, 3],
/// 'b': [
/// 4,
/// [5],
/// 6,
/// ]
/// 'c': [7, 8],
/// };
///
/// // Over:
/// data = {'a': [1, 2, 3], 'b': [4, [5], 6] 'c': [7, 8]};
///
/// We don't do this for record expressions because those are not unbounded
/// in size and generally represent small aggregations of data where the
/// fields are more "closely" bundled together.
///
/// We don't do this for patterns because it's better to fit a pattern on a
/// single line when possible for parallel cases in switches.
///
/// If [preserveNewlines] is `true`, then any newlines or lack of newlines
/// between pairs of elements in the input are preserved in the output. This
/// is used for collection literals that contain line comments to preserve
/// the author's deliberate structuring, as in:
///
/// matrix = [
/// // X, Y, Z:
/// 1, 2, 3,
/// 4, 5, 6,
/// 7, 8, 9,
/// ];
void writeCollection(
Token leftBracket,
List<AstNode> elements,
Token rightBracket, {
Token? constKeyword,
TypeArgumentList? typeArguments,
ListStyle style = const ListStyle(),
bool splitEagerly = false,
bool preserveNewlines = false,
}) {
pieces.modifier(constKeyword);
pieces.visit(typeArguments);
// If the collection is completely empty, write the brackets inline so we
// create fewer pieces. The early return here also means that an empty
// collection won't affect nesting and force outer collections to split.
if (!elements.canSplit(rightBracket)) {
pieces.token(leftBracket);
pieces.token(rightBracket);
return;
}
// Add this collection to the stack.
if (splitEagerly) {
_contents.beginCollection(
isNamed: parentContext == NodeContext.namedExpression,
);
}
var collection = pieces.build(() {
writeList(
leftBracket: leftBracket,
elements,
rightBracket: rightBracket,
style: style,
preserveNewlines: preserveNewlines,
);
});
if (splitEagerly && _contents.endCollection(elements)) {
collection.pin(State.split);
}
pieces.add(collection);
}
/// Creates a comma-separated [ListPiece] for [nodes].
Piece createCommaSeparated(Iterable<AstNode> nodes) {
var builder = DelimitedListBuilder(
this,
const ListStyle(commas: Commas.nonTrailing),
);
nodes.forEach(builder.visit);
return builder.build();
}
/// Writes the leading keyword and parenthesized expression at the beginning
/// of an `if`, `while`, or `switch` expression or statement.
void writeControlFlowStart(
Token keyword,
Token leftParenthesis,
Expression value,
Token rightParenthesis,
) {
pieces.token(keyword);
pieces.space();
pieces.token(leftParenthesis);
pieces.visit(value);
pieces.token(rightParenthesis);
}
/// Writes a dotted or qualified identifier.
void writeDotted(NodeList<SimpleIdentifier> components) {
for (var component in components) {
// Write the preceding ".".
if (component != components.first) {
pieces.token(component.beginToken.previous!);
}
pieces.visit(component);
}
}
/// Creates a [Piece] for an enum constant.
///
/// If [commaAfter] is `true`, then a comma after the constant is written, if
/// present. If the constant is the last constant in an enum declaration that
/// also declares members (and preserve trailing commas is off), then
/// [semicolon] is the `;` token before the members and will be written
/// after the constant.
Piece createEnumConstant(
EnumConstantDeclaration node, {
bool commaAfter = false,
Token? semicolon,
}) {
return pieces.build(metadata: node.metadata, () {
pieces.token(node.name);
if (node.arguments case var arguments?) {
pieces.visit(arguments.typeArguments);
pieces.visit(arguments.constructorSelector);
pieces.visit(arguments.argumentList);
}
if (commaAfter) {
pieces.token(node.commaAfter);
} else if (semicolon != null) {
// Discard the trailing comma if there is one since there is a
// semicolon to use as the separator, but preserve any comments before
// the discarded comma.
pieces.add(
pieces.tokenPiece(discardedToken: node.commaAfter, semicolon),
);
}
});
}
/// Writes a piece for a for statement or element.
void writeFor({
required Token? awaitKeyword,
required Token forKeyword,
required Token leftParenthesis,
required ForLoopParts forLoopParts,
required Token rightParenthesis,
required AstNode body,
required bool hasBlockBody,
bool forceSplitBody = false,
}) {
var forKeywordPiece = pieces.build(() {
pieces.modifier(awaitKeyword);
pieces.token(forKeyword);
});
Piece forPartsPiece;
switch (forLoopParts) {
// Edge case: A totally empty for loop is formatted just as `(;;)` with
// no splits or spaces anywhere.
case ForPartsWithExpression(
initialization: null,
leftSeparator: Token(precedingComments: null),
condition: null,
rightSeparator: Token(precedingComments: null),
updaters: NodeList(isEmpty: true),
)
when rightParenthesis.precedingComments == null:
forPartsPiece = pieces.build(() {
pieces.token(leftParenthesis);
pieces.token(forLoopParts.leftSeparator);
pieces.token(forLoopParts.rightSeparator);
pieces.token(rightParenthesis);
});
case ForParts forParts &&
ForPartsWithDeclarations(variables: AstNode? initializer):
case ForParts forParts &&
ForPartsWithExpression(initialization: AstNode? initializer):
case ForParts forParts &&
ForPartsWithPattern(variables: AstNode? initializer):
// In a C-style for loop, treat the for loop parts like an argument list
// where each clause is a separate argument. This means that when they
// split, they split like:
//
// for (
// initializerClause;
// conditionClause;
// incrementClause
// ) {
// body;
// }
var partsList = DelimitedListBuilder(
this,
const ListStyle(commas: Commas.none),
);
partsList.leftBracket(leftParenthesis);
// The initializer clause.
if (initializer != null) {
partsList.addCommentsBefore(initializer.beginToken);
partsList.add(
pieces.build(() {
pieces.visit(initializer, context: NodeContext.forLoopVariable);
pieces.token(forParts.leftSeparator);
}),
);
} else {
// No initializer, so look at the comments before `;`.
partsList.addCommentsBefore(forParts.leftSeparator);
partsList.add(tokenPiece(forParts.leftSeparator));
}
// The condition clause.
if (forParts.condition case var conditionExpression?) {
partsList.addCommentsBefore(conditionExpression.beginToken);
partsList.add(
pieces.build(() {
pieces.visit(conditionExpression);
pieces.token(forParts.rightSeparator);
}),
);
} else {
partsList.addCommentsBefore(forParts.rightSeparator);
partsList.add(tokenPiece(forParts.rightSeparator));
}
// The update clauses.
if (forParts.updaters.isNotEmpty) {
partsList.addCommentsBefore(forParts.updaters.first.beginToken);
// Unlike most places in the language, if the updaters split, we
// don't want to add a trailing comma. But if the user has preserve
// trailing commas on, we should preserve the comma if there is one
// but not add one if there isn't and it splits.
var listStyle = const ListStyle(commas: Commas.nonTrailing);
if (style.preserveTrailingCommaAfterForUpdaters &&
rightParenthesis.hasCommaBefore) {
listStyle = const ListStyle(commas: Commas.trailing);
}
// Create a nested list builder for the updaters so that they can
// remain unsplit even while the clauses split.
var updaterBuilder = DelimitedListBuilder(this, listStyle);
forParts.updaters.forEach(updaterBuilder.visit);
// Add the updater builder to the clause builder so that any comments
// around a trailing comma after the updaters don't get dropped.
partsList.addInnerBuilder(
updaterBuilder,
forceSplit: style.preserveTrailingCommaBefore(rightParenthesis),
);
}
partsList.rightBracket(rightParenthesis);
forPartsPiece = partsList.build();
case ForEachParts forEachParts &&
ForEachPartsWithDeclaration(:var loopVariable):
forPartsPiece = pieces.build(() {
pieces.token(leftParenthesis);
_writeDeclaredForIn(
loopVariable,
forEachParts.inKeyword,
forEachParts.iterable,
);
pieces.token(rightParenthesis);
});
case ForEachParts forEachParts &&
ForEachPartsWithIdentifier(:var identifier):
// If a for-in loop, treat the for parts like an assignment, so they
// split like:
//
// for (var variable in [
// initializer,
// ]) {
// body;
// }
// TODO(rnystrom): Passing `canBlockSplitLeft: true` allows output like:
//
// // 1
// for (variable in longExpression +
// thatWraps) {
// ...
// }
//
// Versus the split in the initializer forcing a split before `in` too:
//
// // 2
// for (variable
// in longExpression +
// thatWraps) {
// ...
// }
//
// This is also allowed:
//
// // 3
// for (variable
// in longExpression + thatWraps) {
// ...
// }
//
// Currently, the formatter prefers 1 over 3. We may want to revisit
// that and prefer 3 instead. Or perhaps we shouldn't pass
// `canBlockSplitLeft: true` and force the `in` to split if the
// initializer does. That would be consistent with how we handle
// splitting before `case` when the pattern has a newline in an if-case
// statement or element.
forPartsPiece = pieces.build(() {
pieces.token(leftParenthesis);
_writeForIn(
identifier,
forEachParts.inKeyword,
forEachParts.iterable,
);
pieces.token(rightParenthesis);
});
case ForEachParts forEachParts &&
ForEachPartsWithPattern(:var keyword, :var metadata, :var pattern):
forPartsPiece = pieces.build(() {
pieces.token(leftParenthesis);
// Hoist any leading comments so they don't force the for-in clauses
// to split.
pieces.hoistLeadingComments(
metadata.firstOrNull?.beginToken ?? keyword,
() {
// Use a nested piece so that the metadata precedes the keyword
// and not the `(`.
return pieces.build(metadata: metadata, inlineMetadata: true, () {
pieces.token(keyword);
pieces.space();
_writeForIn(
pattern,
forEachParts.inKeyword,
forEachParts.iterable,
);
});
},
);
pieces.token(rightParenthesis);
});
}
var bodyPiece = nodePiece(body);
// If there is metadata before the for loop variable or pattern, then make
// sure that the entire contents of the for loop parts are indented so that
// the annotations are indented.
var indentHeader = switch (forLoopParts) {
ForEachPartsWithDeclaration(:var loopVariable) =>
loopVariable.metadata.isNotEmpty,
ForEachPartsWithPattern(:var metadata) => metadata.isNotEmpty,
_ => false,
};
if (hasBlockBody) {
pieces.add(
ForPiece(forKeywordPiece, forPartsPiece, indent: indentHeader),
);
pieces.space();
pieces.add(bodyPiece);
} else {
var forPiece = ControlFlowPiece();
forPiece.add(
ForPiece(forKeywordPiece, forPartsPiece, indent: indentHeader),
bodyPiece,
isBlock: false,
);
if (forceSplitBody) forPiece.pin(State.split);
pieces.add(forPiece);
}
}
/// Writes a normal (not function-typed) formal parameter with a name and/or
/// type annotation.
///
/// If [mutableKeyword] is given, it should be the `var` or `final` keyword.
/// If [fieldKeyword] and [period] are given, the former should be the `this`
/// or `super` keyword for an initializing formal or super parameter.
void writeFormalParameter(
FormalParameter node,
TypeAnnotation? type,
Token? name, {
Token? mutableKeyword,
Token? fieldKeyword,
Token? period,
}) {
// If the parameter has a default value, the parameter node will be wrapped
// in a DefaultFormalParameter node containing the default.
(Token separator, Expression value)? defaultValueRecord;
if (node.parent case DefaultFormalParameter(
:var separator?,
:var defaultValue?,
)) {
defaultValueRecord = (separator, defaultValue);
}
writeParameter(
metadata: node.metadata,
modifiers: [node.requiredKeyword, node.covariantKeyword, mutableKeyword],
type,
fieldKeyword: fieldKeyword,
period: period,
name,
defaultValue: defaultValueRecord,
);
}
/// Writes a function, method, getter, or setter declaration.
///
/// If [modifierKeyword] is given, it should be the `static` or `abstract`
/// modifier on a method declaration. If [operatorKeyword] is given, it
/// should be the `operator` keyword on an operator declaration. If
/// [propertyKeyword] is given, it should be the `get` or `set` keyword on a
/// getter or setter declaration.
void writeFunction({
List<Annotation> metadata = const [],
List<Token?> modifiers = const [],
TypeAnnotation? returnType,
Token? operatorKeyword,
Token? propertyKeyword,
Token? name,
TypeParameterList? typeParameters,
FormalParameterList? parameters,
required FunctionBody body,
}) {
// Create a piece to attach metadata to the function.
pieces.withMetadata(metadata, () {
writeFunctionAndReturnType(modifiers, returnType, () {
// If there's no return type, attach modifiers to the signature.
if (returnType == null) {
for (var keyword in modifiers) {
pieces.modifier(keyword);
}
}
pieces.modifier(operatorKeyword);
pieces.modifier(propertyKeyword);
pieces.token(name);
pieces.visit(typeParameters);
pieces.visit(parameters);
pieces.visit(body);
});
});
}
/// Writes a return type followed by either a function signature (when writing
/// a function type annotation or function-typed formal) or a signature and a
/// body (when writing a function declaration).
///
/// The [writeFunction] callback should write the function's signature and
/// body if there is one.
///
/// If there is no return type, invokes [writeFunction] directly and returns.
/// Otherwise, writes the return type and function and wraps them in a piece
/// to allow splitting after the return type.
void writeFunctionAndReturnType(
List<Token?> modifiers,
TypeAnnotation? returnType,
void Function() writeFunction,
) {
if (returnType == null) {
writeFunction();
return;
}
// Hoist any comments before the function so they don't force a split
// between the return type and function. In most cases, this doesn't matter
// because the [SequenceBuilder] for the surrounding code will separate out
// the leading comment. But if there is a metadata annotation followed by
// a comment, then the function, then the comment doesn't get captured by
// the [SequenceBuilder], as in:
//
// @meta
// // Weird place for comment.
// int f() {}
var firstToken =
modifiers.nonNulls.firstOrNull ?? returnType.firstNonCommentToken;
pieces.hoistLeadingComments(firstToken, () {
var returnTypePiece = pieces.build(() {
for (var keyword in modifiers) {
pieces.modifier(keyword);
}
pieces.visit(returnType);
});
var signature = pieces.build(() {
writeFunction();
});
return VariablePiece(
returnTypePiece,
[signature],
hasType: true,
is3Dot7: style.is3Dot7,
);
});
}
/// If [parameter] has a [defaultValue] then writes a piece for the parameter
/// followed by that default value.
///
/// Otherwise, just writes [parameter].
void writeDefaultValue(
Piece parameter,
(Token separator, Expression value)? defaultValue,
) {
if (defaultValue == null) {
pieces.add(parameter);
return;
}
var (separator, value) = defaultValue;
var operatorPiece = pieces.build(() {
if (!style.is3Dot7) pieces.add(parameter);
if (separator.type == TokenType.EQ) pieces.space();
pieces.token(separator);
if (separator.type != TokenType.EQ) pieces.space();
});
var valuePiece = nodePiece(value, context: NodeContext.assignment);
if (style.is3Dot7) {
pieces.add(
AssignPiece3Dot7(
left: parameter,
operatorPiece,
valuePiece,
canBlockSplitRight: value.canBlockSplit,
),
);
} else {
pieces.add(AssignPiece(operatorPiece, valuePiece));
}
}
/// Writes a function type or function-typed formal.
///
/// If creating a piece for a function-typed formal, then [parameter] is the
/// formal parameter. If there is a default value, then [defaultValue] is
/// the `=` or `:` separator followed by the constant expression.
///
/// If this is a function-typed initializing formal (`this.foo()`), then
/// [fieldKeyword] is `this` and [period] is the `.`. Likewise, for a
/// function-typed super parameter, [fieldKeyword] is `super`.
void writeFunctionType(
TypeAnnotation? returnType,
Token functionKeywordOrName,
TypeParameterList? typeParameters,
FormalParameterList parameters,
Token? question, {
FormalParameter? parameter,
Token? fieldKeyword,
Token? period,
}) {
var metadata = parameter?.metadata ?? const <Annotation>[];
pieces.withMetadata(metadata, inlineMetadata: true, () {
void write() {
// If there's no return type, attach the parameter modifiers to the
// signature.
if (parameter != null && returnType == null) {
pieces.modifier(parameter.requiredKeyword);
pieces.modifier(parameter.covariantKeyword);
}
pieces.token(fieldKeyword);
pieces.token(period);
pieces.token(functionKeywordOrName);
pieces.visit(typeParameters);
pieces.visit(parameters);
pieces.token(question);
}
var returnTypeModifiers =
parameter != null
? [parameter.requiredKeyword, parameter.covariantKeyword]
: const <Token?>[];
// If the type is a function-typed parameter with a default value, then
// grab the default value from the parent node and attach it to the
// function.
if (parameter?.parent case DefaultFormalParameter(
:var separator?,
:var defaultValue?,
)) {
var function = pieces.build(() {
writeFunctionAndReturnType(returnTypeModifiers, returnType, write);
});
writeDefaultValue(function, (separator, defaultValue));
} else {
writeFunctionAndReturnType(returnTypeModifiers, returnType, write);
}
});
}
/// Writes a parenthesized expression or pattern.
void writeParenthesized(
Token leftBracket,
AstNode content,
Token rightBracket,
) {
pieces.token(leftBracket);
pieces.visit(content);
pieces.token(rightBracket);
}
/// Writes a piece for the header -- everything from the `if` keyword to the
/// closing `)` -- of an if statement, if element, if-case statement, or
/// if-case element.
void writeIfCondition(
Token ifKeyword,
Token leftParenthesis,
Expression expression,
CaseClause? caseClause,
Token rightParenthesis,
) {
pieces.token(ifKeyword);
pieces.space();
pieces.token(leftParenthesis);
if (caseClause != null) {
var expressionPiece = nodePiece(expression);
var casePiece = pieces.build(() {
pieces.token(caseClause.caseKeyword);
pieces.space();
pieces.visit(caseClause.guardedPattern.pattern);
});
var guardPiece = optionalNodePiece(caseClause.guardedPattern.whenClause);
pieces.add(
IfCasePiece(
expressionPiece,
casePiece,
guardPiece,
canBlockSplitPattern: caseClause.guardedPattern.pattern.canBlockSplit,
),
);
} else {
pieces.visit(expression);
}
pieces.token(rightParenthesis);
}
/// Writes a [TryPiece] for try statement.
void writeTry(TryStatement tryStatement) {
pieces.token(tryStatement.tryKeyword);
pieces.space();
writeBlock(tryStatement.body);
for (var i = 0; i < tryStatement.catchClauses.length; i++) {
var catchClause = tryStatement.catchClauses[i];
pieces.space();
if (catchClause.onKeyword case var onKeyword?) {
pieces.token(onKeyword, spaceAfter: true);
pieces.visit(catchClause.exceptionType);
}
if (catchClause.onKeyword != null && catchClause.catchKeyword != null) {
pieces.space();
}
if (catchClause.catchKeyword case var catchKeyword?) {
pieces.token(catchKeyword);
pieces.space();
var parameters = DelimitedListBuilder(
this,
const ListStyle(commas: Commas.nonTrailing),
);
parameters.leftBracket(catchClause.leftParenthesis!);
if (catchClause.exceptionParameter case var exceptionParameter?) {
parameters.visit(exceptionParameter);
}
if (catchClause.stackTraceParameter case var stackTraceParameter?) {
parameters.visit(stackTraceParameter);
}
parameters.rightBracket(catchClause.rightParenthesis!);
pieces.add(parameters.build());
}
pieces.space();
// Edge case: When there's another catch/on/finally after this one, we
// want to force the block to split even if it's empty.
//
// try {
// ..
// } on Foo {
// } finally Bar {
// body;
// }
var forceSplit =
i < tryStatement.catchClauses.length - 1 ||
tryStatement.finallyBlock != null;
writeBlock(catchClause.body, forceSplit: forceSplit);
}
if (tryStatement.finallyBlock case var finallyBlock?) {
pieces.space();
pieces.token(tryStatement.finallyKeyword!);
pieces.space();
writeBlock(finallyBlock);
}
}
/// Writes an [ImportPiece] for an import or export directive.
void writeImport(
NamespaceDirective directive,
Token keyword, {
Token? deferredKeyword,
Token? asKeyword,
SimpleIdentifier? prefix,
}) {
pieces.withMetadata(directive.metadata, () {
// Build a piece for the directive itself.
var directivePiece = pieces.build(() {
pieces.token(keyword);
pieces.space();
pieces.visit(directive.uri);
});
// Add all of the clauses and combinators.
var clauses = <Piece>[];
// Include any `if` clauses.
for (var configuration in directive.configurations) {
clauses.add(nodePiece(configuration));
}
// Include the `as` clause.
if (asKeyword != null) {
clauses.add(
pieces.build(() {
pieces.token(deferredKeyword, spaceAfter: true);
pieces.token(asKeyword);
pieces.space();
pieces.visit(prefix!);
}),
);
}
// Include the `show` and `hide` clauses.
for (var combinatorNode in directive.combinators) {
switch (combinatorNode) {
case HideCombinator(hiddenNames: var names):
case ShowCombinator(shownNames: var names):
clauses.add(
InfixPiece([
tokenPiece(combinatorNode.keyword),
for (var name in names)
tokenPiece(name.token, commaAfter: true),
], is3Dot7: style.is3Dot7),
);
}
}
// If there are clauses, include them.
if (clauses.isNotEmpty) {
pieces.add(ClausePiece(directivePiece, clauses));
} else {
pieces.add(directivePiece);
}
pieces.token(directive.semicolon);
});
}
/// Writes a [Piece] for an index expression.
void writeIndexExpression(IndexExpression index) {
// TODO(rnystrom): Consider whether we should allow splitting between
// successive index expressions, like:
//
// jsonData['some long key']
// ['another long key'];
//
// The current formatter allows it, but it's very rarely used (0.021% of
// index expressions in a corpus of pub packages).
pieces.token(index.question);
pieces.token(index.period);
if (style.is3Dot7) {
pieces.token(index.leftBracket);
pieces.visit(index.index);
pieces.token(index.rightBracket);
} else {
// Wrap the index expression in a [GroupingPiece] so that a split inside
// the index doesn't cause the surrounding piece to have a certain shape.
pieces.add(
GroupingPiece(
pieces.build(() {
pieces.token(index.leftBracket);
pieces.visit(index.index);
pieces.token(index.rightBracket);
}),
),
);
}
}
/// Writes a single infix operation.
///
/// If [hanging] is `true` then the operator goes at the end of the first
/// line, like `+`. Otherwise, it goes at the beginning of the second, like
/// `as`.
///
/// The [operator2] parameter may be passed if the "operator" is actually two
/// separate tokens, as in `foo is! Bar`.
void writeInfix(
AstNode left,
Token operator,
AstNode right, {
bool hanging = false,
Token? operator2,
Indent indent = Indent.infix,
}) {
var leftPiece = pieces.build(() {
pieces.visit(left);
if (hanging) {
pieces.space();
pieces.token(operator);
pieces.token(operator2);
}
});
var rightPiece = pieces.build(() {
if (!hanging) {
pieces.token(operator);
pieces.token(operator2);
pieces.space();
}
pieces.visit(right);
});
pieces.add(
InfixPiece(
[leftPiece, rightPiece],
indent: indent,
is3Dot7: style.is3Dot7,
),
);
}
/// Writes a chained infix operation: a binary operator expression, or
/// binary pattern.
///
/// In a tree of binary AST nodes, all operators at the same precedence are
/// treated as a single chain of operators that either all split or none do.
/// Operands within those (which may themselves be chains of higher
/// precedence binary operators) are then formatted independently.
///
/// [T] is the type of node being visited and [destructure] is a callback
/// that takes one of those and yields the operands and operator. We need
/// this since there's no interface shared by the various binary operator
/// AST nodes.
///
/// If [precedence] is given, then this only flattens binary nodes with that
/// same precedence.
void writeInfixChain<T extends AstNode>(
T node,
BinaryOperation Function(T node) destructure, {
int? precedence,
bool indent = true,
}) {
var operands = <Piece>[];
void traverse(AstNode e) {
// If the node is one if our infix operators, then recurse into the
// operands.
if (e is T) {
var (left, operator, right) = destructure(e);
if (precedence == null || operator.type.precedence == precedence) {
operands.add(
pieces.build(() {
traverse(left);
pieces.space();
pieces.token(operator);
}),
);
traverse(right);
return;
}
}
// Otherwise, just write the node itself.
pieces.visit(e);
}
operands.add(
pieces.build(() {
traverse(node);
}),
);
pieces.add(
InfixPiece(
operands,
indent: indent ? Indent.infix : Indent.none,
is3Dot7: style.is3Dot7,
),
);
}
/// Writes a [ListPiece] for the given bracket-delimited set of elements.
///
/// If [preserveNewlines] is `true`, then any newlines or lack of newlines
/// between pairs of elements in the input are preserved in the output. This
/// is used for collection literals that contain line comments to preserve
/// the author's deliberate structuring, as in:
///
/// matrix = [
/// 1, 2, 3, //
/// 4, 5, 6,
/// 7, 8, 9,
/// ];
void writeList(
List<AstNode> elements, {
required Token leftBracket,
required Token rightBracket,
ListStyle style = const ListStyle(),
bool preserveNewlines = false,
bool allowBlockArgument = false,
bool blockShaped = true,
}) {
// If the list is completely empty, write the brackets directly inline so
// that we create fewer pieces.
if (!elements.canSplit(rightBracket)) {
pieces.token(leftBracket);
pieces.token(rightBracket);
return;
}
var builder = DelimitedListBuilder(this, style);
builder.leftBracket(leftBracket);
if (preserveNewlines && elements.containsLineComments(rightBracket)) {
_preserveNewlinesInCollection(elements, builder);
} else {
builder.visitAll(elements, allowBlockArgument: allowBlockArgument);
}
builder.rightBracket(rightBracket);
pieces.add(
builder.build(
// If we are always writing a trailing comma (because it's a
// single-element record), then the comma shouldn't force a split.
forceSplit:
style.commas != Commas.alwaysTrailing &&
this.style.preserveTrailingCommaBefore(rightBracket),
blockShaped: blockShaped,
),
);
}
/// Writes [elements] into [builder], preserving the original newlines (or
/// lack thereof) between elements.
///
/// This is used for formatting collection literals that contain at least one
/// line comment between elements. In that case, we use the line comment as a
/// single to prefer the author's chosen newlines between elements. For
/// example, if the user writes:
///
/// list = [
/// 1,2, 3, 4,
/// // comment
/// 5,6, 7
/// ];
///
/// The formatter produces:
///
/// list = [
/// 1, 2, 3, 4,
/// // comment
/// 5, 6, 7
/// ];
void _preserveNewlinesInCollection(
List<AstNode> elements,
DelimitedListBuilder builder,
) {
// Builder for all of the elements on a single line. We use a ListPiece for
// this too because even though we prefer to keep all elements that are on
// a single line in the input also on a single line in the output, we will
// split them if they don't fit.
var lineStyle = const ListStyle(commas: Commas.nonTrailing);
var lineBuilder = DelimitedListBuilder(this, lineStyle);
var atLineStart = true;
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (!atLineStart &&
comments.hasNewlineBetween(
elements[i - 1].endToken,
element.beginToken,
)) {
// This element begins a new line. Add the elements on the previous
// line to the list builder and start a new line.
builder.addInnerBuilder(lineBuilder);
lineBuilder = DelimitedListBuilder(this, lineStyle);
atLineStart = true;
}
// Let the main list builder handle comments that occur between elements
// that aren't on the same line.
if (atLineStart) builder.addCommentsBefore(element.beginToken);
lineBuilder.visit(element);
// There is an element on this line now.
atLineStart = false;
}
// Finish the last line if there is anything on it.
if (!atLineStart) builder.addInnerBuilder(lineBuilder);
}
/// Writes a [VariablePiece] for a named or wildcard variable pattern.
void writePatternVariable(Token? keyword, TypeAnnotation? type, Token name) {
// If it's a wildcard with no declaration keyword or type, there is just a
// name token.
if (keyword == null && type == null) {
pieces.token(name);
return;
}
var header = pieces.build(() {
pieces.modifier(keyword);
pieces.visit(type);
});
pieces.add(
VariablePiece(
header,
[tokenPiece(name)],
hasType: type != null,
is3Dot7: style.is3Dot7,
),
);
}
/// Writes a [Piece] for an AST node followed by an unsplittable token.
void writePostfix(AstNode node, Token? operator) {
pieces.visit(node);
pieces.token(operator);
}
/// Writes a [Piece] for an AST node preceded by an unsplittable token.
///
/// If [space] is `true` and there is an operator, writes a space between the
/// operator and operand.
void writePrefix(Token? operator, AstNode? operand, {bool space = false}) {
if (style.is3Dot7) {
pieces.token(operator, spaceAfter: space);
pieces.visit(operand);
return;
}
// Wrap in grouping so that an infix split inside the prefix operator
// doesn't get collapsed in the surrounding context, like:
//
// // Wrong:
// var x =
// throw 'Some long string '
// 'wrapped.';
//
// // Right:
// var x =
// throw 'Some long string '
// 'wrapped.';
pieces.add(
PrefixPiece(
pieces.build(() {
pieces.token(operator, spaceAfter: space);
pieces.visit(operand);
}),
),
);
}
/// Writes an [AdjacentPiece] for a given record type field.
void writeRecordTypeField(RecordTypeAnnotationField node) {
writeParameter(metadata: node.metadata, node.type, node.name);
}
/// Writes a [ListPiece] for a record literal or pattern.
void writeRecord(
Token leftParenthesis,
List<AstNode> fields,
Token rightParenthesis, {
Token? constKeyword,
bool preserveNewlines = false,
}) {
var style = switch (fields) {
// Record types or patterns with a single named field don't add a trailing
// comma unless it's split, like:
//
// ({int n}) x;
//
// Or:
//
// if (obj case (name: value)) {
// ;
// }
[PatternField(name: _?)] => const ListStyle(commas: Commas.trailing),
[NamedExpression()] => const ListStyle(commas: Commas.trailing),
// Record types or patterns with a single positional field always have a
// trailing comma to disambiguate from parenthesized expressions or
// patterns, like:
//
// (int,) x;
//
// Or:
//
// if (obj case (pattern,)) {
// ;
// }
[_] => const ListStyle(commas: Commas.alwaysTrailing),
// Record types or patterns with multiple fields have regular trailing
// commas when split.
_ => const ListStyle(commas: Commas.trailing),
};
writeCollection(
constKeyword: constKeyword,
leftParenthesis,
fields,
rightParenthesis,
style: style,
preserveNewlines: preserveNewlines,
);
}
/// Writes a class, enum, extension, extension type, mixin, or mixin
/// application class declaration.
///
/// The [keywords] list is the ordered list of modifiers and keywords at the
/// beginning of the declaration.
///
/// For all but a mixin application class, [body] should a record containing
/// the bracket delimiters and the list of member declarations for the type's
/// body. For mixin application classes, [body] is `null` and instead
/// [equals], [superclass], and [semicolon] are provided.
///
/// If the type is an extension, then [onType] is a record containing the
/// `on` keyword and the on type.
///
/// If the type is an extension type, then [representation] is the primary
/// constructor for it.
void writeType(
NodeList<Annotation> metadata,
List<Token?> keywords,
Token? name, {
TypeParameterList? typeParameters,
Token? equals,
NamedType? superclass,
RepresentationDeclaration? representation,
ExtendsClause? extendsClause,
MixinOnClause? onClause,
WithClause? withClause,
ImplementsClause? implementsClause,
NativeClause? nativeClause,
(Token, TypeAnnotation)? onType,
TypeBodyType bodyType = TypeBodyType.block,
required Piece Function() body,
}) {
// Begin a piece to attach the metadata to the type.
pieces.withMetadata(metadata, () {
var header = pieces.build(() {
var space = false;
for (var keyword in keywords) {
if (space) pieces.space();
pieces.token(keyword);
if (keyword != null) space = true;
}
pieces.token(name, spaceBefore: true);
if (typeParameters != null) {
pieces.visit(typeParameters);
}
// Mixin application classes have ` = Superclass` after the declaration
// name.
if (equals != null) {
pieces.space();
pieces.token(equals);
pieces.space();
pieces.visit(superclass!);
}
// Extension types have a representation type.
if (representation != null) {
pieces.visit(representation);
}
});
var clauses = <Piece>[];
void typeClause(Token keyword, List<AstNode> types) {
clauses.add(
InfixPiece([
tokenPiece(keyword),
for (var type in types) nodePiece(type, commaAfter: true),
], is3Dot7: style.is3Dot7),
);
}
if (extendsClause != null) {
typeClause(extendsClause.extendsKeyword, [extendsClause.superclass]);
}
if (onClause != null) {
typeClause(onClause.onKeyword, onClause.superclassConstraints);
}
if (withClause != null) {
typeClause(withClause.withKeyword, withClause.mixinTypes);
}
if (implementsClause != null) {
typeClause(
implementsClause.implementsKeyword,
implementsClause.interfaces,
);
}
if (onType case (var onKeyword, var onType)?) {
typeClause(onKeyword, [onType]);
}
if (nativeClause != null) {
typeClause(nativeClause.nativeKeyword, [
if (nativeClause.name case var name?) name,
]);
}
if (clauses.isNotEmpty) {
header = ClausePiece(
header,
clauses,
allowLeadingClause: extendsClause != null || onClause != null,
);
}
pieces.add(TypePiece(header, body(), bodyType: bodyType));
});
}
/// Writes a [ListPiece] for a type argument or type parameter list.
void writeTypeList(
Token leftBracket,
List<AstNode> elements,
Token rightBracket,
) {
writeList(
leftBracket: leftBracket,
elements,
rightBracket: rightBracket,
style: const ListStyle(commas: Commas.nonTrailing, splitCost: 3),
blockShaped: false,
);
}
/// Handles the `async`, `sync*`, or `async*` modifiers on a function body.
void writeFunctionBodyModifiers(FunctionBody body) {
// The `async` or `sync` keyword.
pieces.token(body.keyword);
pieces.token(body.star);
if (body.keyword != null) pieces.space();
}
/// Writes a [Piece] with "assignment-like" splitting.
///
/// This is used, obviously, for assignments and variable declarations to
/// handle splitting after the `=`, but is also used in any context where an
/// expression follows something that it "defines" or "initializes":
///
/// * Assignment
/// * Variable declaration
/// * Constructor initializer
/// * Expression (`=>`) function body
/// * Named argument or named record field (`:`)
/// * Map entry (`:`)
void writeAssignment(
AstNode leftHandSide,
Token operator,
AstNode rightHandSide, {
bool includeComma = false,
NodeContext leftHandSideContext = NodeContext.none,
NodeContext rightHandSideContext = NodeContext.none,
}) {
if (style.is3Dot7) {
_writeAssignment3Dot7(
leftHandSide,
operator,
rightHandSide,
includeComma: includeComma,
leftHandSideContext: leftHandSideContext,
);
return;
}
var leftPiece = pieces.build(() {
pieces.visit(leftHandSide, context: leftHandSideContext);
if (operator.type != TokenType.COLON) pieces.space();
pieces.token(operator);
});
var rightPiece = nodePiece(
rightHandSide,
commaAfter: includeComma,
context: rightHandSideContext,
);
pieces.add(AssignPiece(leftPiece, rightPiece));
}
/// Writes the `<variable> in <expression>` part of an identifier or pattern
/// for-in loop.
void _writeForIn(AstNode leftHandSide, Token inKeyword, Expression sequence) {
// Hoist any leading comments so they don't force the for-in clauses to
// split.
pieces.hoistLeadingComments(leftHandSide.firstNonCommentToken, () {
var leftPiece = nodePiece(
leftHandSide,
context: NodeContext.forLoopVariable,
);
var sequencePiece = _createForInSequence(inKeyword, sequence);
return ForInPiece(
leftPiece,
sequencePiece,
canBlockSplitSequence: sequence.canBlockSplit,
is3Dot7: style.is3Dot7,
);
});
}
/// Writes the `<variable> in <expression>` part of a for-in loop when the
/// part before `in` is a variable declaration.
///
/// A for-in loop with a variable declaration can have metadata before it,
/// which requires some special handling so that we don't push the metadata
/// and any comments after it into the left child piece of [ForInPiece].
void _writeDeclaredForIn(
DeclaredIdentifier identifier,
Token inKeyword,
Expression sequence,
) {
// Hoist any leading comments so they don't force the for-in clauses
// to split.
pieces.hoistLeadingComments(identifier.beginToken, () {
// Use a nested piece so that the metadata precedes the keyword and
// not the `(`.
return pieces.build(
metadata: identifier.metadata,
inlineMetadata: true,
() {
var leftPiece = pieces.build(() {
writeParameter(
modifiers: [identifier.keyword],
identifier.type,
identifier.name,
);
});
var sequencePiece = _createForInSequence(inKeyword, sequence);
pieces.add(
ForInPiece(
leftPiece,
sequencePiece,
canBlockSplitSequence: sequence.canBlockSplit,
is3Dot7: style.is3Dot7,
),
);
},
);
});
}
/// Creates a piece for the `in <sequence>` part of a for-in loop.
Piece _createForInSequence(Token inKeyword, Expression sequence) {
return pieces.build(() {
// Put the `in` at the beginning of the sequence.
pieces.token(inKeyword);
pieces.space();
pieces.visit(sequence);
});
}
void _writeAssignment3Dot7(
AstNode leftHandSide,
Token operator,
AstNode rightHandSide, {
bool includeComma = false,
NodeContext leftHandSideContext = NodeContext.none,
}) {
// If an operand can have block formatting, then a newline in it doesn't
// force the operator to split, as in:
//
// var [
// element,
// ] = list;
//
// Or:
//
// var list = [
// element,
// ];
var canBlockSplitLeft = switch (leftHandSide) {
// Treat method chains and cascades on the LHS as if they were blocks.
// They don't really fit the "block" term, but it looks much better to
// force a method chain to split on the left than to try to avoid
// splitting it and split at the assignment instead:
//
// // Worse:
// target.method(
// argument,
// ).setter =
// value;
//
// // Better:
// target.method(argument)
// .setter = value;
//
MethodInvocation() => true,
PropertyAccess() => true,
PrefixedIdentifier() => true,
// Otherwise, it must be an actual block construct.
Expression() => leftHandSide.canBlockSplit,
DartPattern() => leftHandSide.canBlockSplit,
_ => false,
};
var canBlockSplitRight = switch (rightHandSide) {
Expression() => rightHandSide.canBlockSplit,
DartPattern() => rightHandSide.canBlockSplit,
_ => false,
};
var leftPiece = nodePiece(leftHandSide, context: leftHandSideContext);
var operatorPiece = pieces.build(() {
if (operator.type != TokenType.COLON) pieces.space();
pieces.token(operator);
});
var rightPiece = nodePiece(
rightHandSide,
commaAfter: includeComma,
context: NodeContext.assignment,
);
pieces.add(
AssignPiece3Dot7(
left: leftPiece,
operatorPiece,
rightPiece,
canBlockSplitLeft: canBlockSplitLeft,
canBlockSplitRight: canBlockSplitRight,
),
);
}
/// Writes a piece for a parameter-like constructor: Either a simple formal
/// parameter or a record type field, which is syntactically similar to a
/// parameter.
///
/// If the parameter has a default value, then [defaultValue] contains the
/// `:` or `=` separator and the constant value expression.
void writeParameter(
TypeAnnotation? type,
Token? name, {
List<Annotation> metadata = const [],
List<Token?> modifiers = const [],
Token? fieldKeyword,
Token? period,
(Token separator, Expression value)? defaultValue,
}) {
// Begin a piece to attach metadata to the parameter.
pieces.withMetadata(metadata, inlineMetadata: true, () {
Piece? typePiece;
if (type != null) {
typePiece = pieces.build(() {
for (var keyword in modifiers) {
pieces.modifier(keyword);
}
pieces.visit(type);
});
}
Piece? namePiece;
if (name != null) {
namePiece = pieces.build(() {
// If there is a type annotation, the modifiers will be before the
// type. Otherwise, they go before the name.
if (type == null) {
for (var keyword in modifiers) {
pieces.modifier(keyword);
}
}
pieces.token(fieldKeyword);
pieces.token(period);
pieces.token(name);
});
}
Piece parameterPiece;
if (typePiece != null && namePiece != null) {
// We have both a type and name, allow splitting between them.
parameterPiece = VariablePiece(
typePiece,
[namePiece],
hasType: true,
is3Dot7: style.is3Dot7,
);
} else {
// Will have at least a type or name.
parameterPiece = typePiece ?? namePiece!;
}
// If there's a default value, include it. We do that inside here so that
// any metadata surrounds the entire assignment instead of being part of
// the assignment's left-hand side where a split in the metadata would
// force a split at the default value separator.
writeDefaultValue(parameterPiece, defaultValue);
});
}
/// Visits [node] and creates a piece from it.
///
/// If [commaAfter] is `true`, looks for a comma token after [node] and
/// writes it to the piece as well.
Piece nodePiece(
AstNode node, {
bool commaAfter = false,
NodeContext context = NodeContext.none,
}) {
var result = pieces.build(() {
visitNode(node, context);
});
if (commaAfter) {
var nextToken = node.endToken.next!;
if (nextToken.lexeme == ',') {
var comma = tokenPiece(nextToken);
result = AdjacentPiece([result, comma]);
}
}
return result;
}
/// Visits [node] and creates a piece from it if not `null`.
///
/// Otherwise returns `null`.
Piece? optionalNodePiece(AstNode? node) {
if (node == null) return null;
return nodePiece(node);
}
/// Creates a piece for only [token].
///
/// If [commaAfter] is `true`, will look for and write a comma following the
/// token if there is one.
Piece tokenPiece(Token token, {bool commaAfter = false}) {
return pieces.tokenPiece(token, commaAfter: commaAfter);
}
}