blob: f8c952c4f5650839bdbc466623c71a539e30126d [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 '../piece/assign.dart';
import '../piece/block.dart';
import '../piece/clause.dart';
import '../piece/function.dart';
import '../piece/infix.dart';
import '../piece/list.dart';
import '../piece/piece.dart';
import '../piece/postfix.dart';
import '../piece/try.dart';
import '../piece/type.dart';
import '../piece/variable.dart';
import 'adjacent_builder.dart';
import 'ast_node_visitor.dart';
import 'comment_writer.dart';
import 'delimited_list_builder.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);
/// 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 {
PieceWriter get pieces;
CommentWriter get comments;
Piece nodePiece(AstNode node, {bool commaAfter = false});
/// Creates a [ListPiece] for an argument list.
Piece createArgumentList(
Token leftBracket, Iterable<AstNode> elements, Token rightBracket) {
return createList(
leftBracket: leftBracket,
elements,
rightBracket: rightBracket,
style: const ListStyle(allowBlockElement: true));
}
/// Creates a [BlockPiece] for a given 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 {}
Piece createBody(
Token leftBracket, List<AstNode> contents, Token rightBracket,
{bool forceSplit = false}) {
var leftBracketPiece = tokenPiece(leftBracket);
var sequence = SequenceBuilder(this);
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();
}
// Place any comments before the "}" inside the block.
sequence.addCommentsBefore(rightBracket);
var rightBracketPiece = tokenPiece(rightBracket);
return BlockPiece(leftBracketPiece, sequence.build(), rightBracketPiece,
alwaysSplit: forceSplit || contents.isNotEmpty || sequence.mustSplit);
}
/// Creates a [BlockPiece] 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 {}
Piece createBlock(Block block, {bool forceSplit = false}) {
return createBody(block.leftBracket, block.statements, block.rightBracket,
forceSplit: forceSplit);
}
/// Creates a piece for a `break` or `continue` statement.
Piece createBreak(Token keyword, SimpleIdentifier? label, Token semicolon) {
return buildPiece((b) {
b.token(keyword);
b.visit(label, spaceBefore: true);
b.token(semicolon);
});
}
/// Creates a [ListPiece] for a collection literal.
Piece createCollection(Token? constKeyword, Token leftBracket,
List<AstNode> elements, Token rightBracket,
{TypeArgumentList? typeArguments, ListStyle style = const ListStyle()}) {
return buildPiece((b) {
b.modifier(constKeyword);
b.visit(typeArguments);
// TODO(tall): Support a line comment inside a collection literal as a
// signal to preserve internal newlines. So if you have:
//
// var list = [
// 1, 2, 3, // comment
// 4, 5, 6,
// ];
//
// The formatter will preserve the newline after element 3 and the lack of
// them after the other elements.
b.add(createList(
leftBracket: leftBracket,
elements,
rightBracket: rightBracket,
style: style,
));
});
}
/// Visits the leading keyword and parenthesized expression at the beginning
/// of an `if`, `while`, or `switch` expression or statement.
Piece startControlFlow(Token keyword, Token leftParenthesis, Expression value,
Token rightParenthesis) {
// Attach the keyword to the `(`.
return buildPiece((b) {
b.token(keyword);
b.space();
b.token(leftParenthesis);
b.visit(value);
b.token(rightParenthesis);
});
}
/// Creates metadata annotations for a directive.
///
/// Always forces the annotations to be on a previous line.
void createDirectiveMetadata(Directive directive) {
// TODO(tall): Implement. See SourceVisitor._visitDirectiveMetadata().
if (directive.metadata.isNotEmpty) throw UnimplementedError();
}
/// Creates a dotted or qualified identifier.
Piece createDotted(NodeList<SimpleIdentifier> components) {
return buildPiece((b) {
for (var component in components) {
// Write the preceding ".".
if (component != components.first) {
b.token(component.beginToken.previous!);
}
b.visit(component);
}
});
}
/// Creates a [Piece] for an enum constant.
///
/// If the constant is in an enum declaration that also declares members, then
/// [hasMembers] should be `true`, [semicolon] is the `;` token before the
/// members (if any), and [isLastConstant] is `true` if [node] is the last
/// constant before the members.
Piece createEnumConstant(EnumConstantDeclaration node,
{bool hasMembers = false,
bool isLastConstant = false,
Token? semicolon}) {
return buildPiece((b) {
b.token(node.name);
if (node.arguments case var arguments?) {
b.visit(arguments.typeArguments);
b.visit(arguments.argumentList);
}
if (hasMembers) {
if (!isLastConstant) {
b.token(node.commaAfter);
} else {
// 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.
b.commentsBefore(node.commaAfter);
b.token(semicolon);
}
}
});
}
/// Creates a piece for the parentheses and inner parts of a for statement or
/// for element.
Piece createForLoopParts(Token leftParenthesis, ForLoopParts forLoopParts,
Token rightParenthesis) {
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:
return buildPiece((b) {
b.token(leftParenthesis);
b.token(forLoopParts.leftSeparator);
b.token(forLoopParts.rightSeparator);
b.token(rightParenthesis);
});
case ForParts forParts &&
ForPartsWithDeclarations(variables: AstNode? initializer):
case ForParts forParts &&
ForPartsWithExpression(initialization: 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(buildPiece((b) {
b.visit(initializer);
b.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(buildPiece((b) {
b.visit(conditionExpression);
b.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);
partsList.add(createList(forParts.updaters,
style: const ListStyle(commas: Commas.nonTrailing)));
}
partsList.rightBracket(rightParenthesis);
return partsList.build();
case ForPartsWithPattern():
throw UnimplementedError();
case ForEachParts forEachParts &&
ForEachPartsWithDeclaration(loopVariable: AstNode variable):
case ForEachParts forEachParts &&
ForEachPartsWithIdentifier(identifier: AstNode variable):
// If a for-in loop, treat the for parts like an assignment, so they
// split like:
//
// for (var variable in [
// initializer,
// ]) {
// body;
// }
// TODO(tall): Passing `allowInnerSplit: 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.
return buildPiece((b) {
b.token(leftParenthesis);
b.add(createAssignment(
variable, forEachParts.inKeyword, forEachParts.iterable,
splitBeforeOperator: true, allowInnerSplit: true));
b.token(rightParenthesis);
});
case ForEachPartsWithPattern():
throw UnimplementedError();
}
}
/// Creates 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.
Piece createFormalParameter(
NormalFormalParameter node, TypeAnnotation? type, Token? name,
{Token? mutableKeyword, Token? fieldKeyword, Token? period}) {
var builder = AdjacentBuilder(this);
startFormalParameter(node, builder);
builder.modifier(mutableKeyword);
builder.visit(type);
Piece? typePiece;
if (type != null && name != null) {
typePiece = builder.build();
}
builder.token(fieldKeyword);
builder.token(period);
builder.token(name);
// If we have both a type and name, allow splitting between them.
if (typePiece != null) {
var namePiece = builder.build();
return VariablePiece(typePiece, [namePiece], hasType: true);
}
return builder.build();
}
/// Creates 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.
Piece createFunction(
{List<Token?> modifiers = const [],
TypeAnnotation? returnType,
Token? operatorKeyword,
Token? propertyKeyword,
Token? name,
TypeParameterList? typeParameters,
FormalParameterList? parameters,
required FunctionBody body}) {
var builder = AdjacentBuilder(this);
for (var keyword in modifiers) {
builder.modifier(keyword);
}
Piece? returnTypePiece;
if (returnType != null) {
builder.visit(returnType);
returnTypePiece = builder.build();
}
builder.modifier(operatorKeyword);
builder.modifier(propertyKeyword);
builder.token(name);
builder.visit(typeParameters);
builder.visit(parameters);
var signature = builder.build();
var bodyPiece = createFunctionBody(body);
return FunctionPiece(returnTypePiece, signature,
isReturnTypeFunctionType: returnType is GenericFunctionType,
body: bodyPiece);
}
/// Creates a piece for a function, method, or constructor body.
Piece createFunctionBody(FunctionBody body) {
return buildPiece((b) {
// Don't put a space before `;` bodies.
if (body is! EmptyFunctionBody) b.space();
b.visit(body);
});
}
/// Creates a function type or function-typed formal.
///
/// If creating a piece for a function-typed formal, then [parameter] is the
/// formal parameter.
///
/// 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`.
Piece createFunctionType(
TypeAnnotation? returnType,
Token functionKeywordOrName,
TypeParameterList? typeParameters,
FormalParameterList parameters,
Token? question,
{FormalParameter? parameter,
Token? fieldKeyword,
Token? period}) {
var builder = AdjacentBuilder(this);
if (parameter != null) startFormalParameter(parameter, builder);
Piece? returnTypePiece;
if (returnType != null) {
builder.visit(returnType);
returnTypePiece = builder.build();
}
builder.token(fieldKeyword);
builder.token(period);
builder.token(functionKeywordOrName);
builder.visit(typeParameters);
builder.visit(parameters);
builder.token(question);
return FunctionPiece(returnTypePiece, builder.build(),
isReturnTypeFunctionType: returnType is GenericFunctionType);
}
/// Creates a [TryPiece] for try statement.
Piece createTry(TryStatement tryStatement) {
var piece = TryPiece();
var tryHeader = tokenPiece(tryStatement.tryKeyword);
var tryBlock = createBlock(tryStatement.body);
piece.add(tryHeader, tryBlock);
for (var i = 0; i < tryStatement.catchClauses.length; i++) {
var catchClause = tryStatement.catchClauses[i];
var catchClauseHeader = buildPiece((b) {
if (catchClause.onKeyword case var onKeyword?) {
b.token(onKeyword, spaceAfter: true);
b.visit(catchClause.exceptionType);
}
if (catchClause.onKeyword != null && catchClause.catchKeyword != null) {
b.space();
}
if (catchClause.catchKeyword case var catchKeyword?) {
b.token(catchKeyword);
b.space();
var parameters = DelimitedListBuilder(this);
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!);
b.add(parameters.build());
}
});
// 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;
var catchClauseBody =
createBlock(catchClause.body, forceSplit: forceSplit);
piece.add(catchClauseHeader, catchClauseBody);
}
if (tryStatement.finallyBlock case var finallyBlock?) {
var finallyHeader = tokenPiece(tryStatement.finallyKeyword!);
var finallyBody = createBlock(finallyBlock);
piece.add(finallyHeader, finallyBody);
}
return piece;
}
/// Creates an [ImportPiece] for an import or export directive.
Piece createImport(NamespaceDirective directive, Token keyword,
{Token? deferredKeyword, Token? asKeyword, SimpleIdentifier? prefix}) {
var builder = AdjacentBuilder(this);
createDirectiveMetadata(directive);
builder.token(keyword);
builder.space();
builder.visit(directive.uri);
if (directive.configurations.isNotEmpty) {
var configurations = <Piece>[];
for (var configuration in directive.configurations) {
configurations.add(nodePiece(configuration));
}
builder.add(PostfixPiece(configurations));
}
if (asKeyword != null) {
builder.add(PostfixPiece([
buildPiece((b) {
b.token(deferredKeyword, spaceAfter: true);
b.token(asKeyword);
b.space();
b.visit(prefix!);
})
]));
}
if (directive.combinators.isNotEmpty) {
var combinators = <ClausePiece>[];
for (var combinatorNode in directive.combinators) {
var combinatorKeyword = tokenPiece(combinatorNode.keyword);
switch (combinatorNode) {
case HideCombinator(hiddenNames: var names):
case ShowCombinator(shownNames: var names):
var parts = <Piece>[];
for (var name in names) {
parts.add(tokenPiece(name.token, commaAfter: true));
}
var combinator = ClausePiece(combinatorKeyword, parts);
combinators.add(combinator);
default:
throw StateError('Unknown combinator type $combinatorNode.');
}
}
builder.add(ClausesPiece(combinators));
}
builder.token(directive.semicolon);
return builder.build();
}
/// Creates 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`.
Piece createInfix(AstNode left, Token operator, AstNode right,
{bool hanging = false, Token? operator2}) {
var leftPiece = buildPiece((b) {
b.visit(left);
if (hanging) {
b.space();
b.token(operator);
b.token(operator2);
}
});
var rightPiece = buildPiece((b) {
if (!hanging) {
b.token(operator);
b.token(operator2);
b.space();
}
b.visit(right);
});
return InfixPiece([leftPiece, rightPiece]);
}
/// Creates 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.
Piece createInfixChain<T extends AstNode>(
T node, BinaryOperation Function(T node) destructure,
{int? precedence}) {
var builder = AdjacentBuilder(this);
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) {
traverse(left);
builder.space();
builder.token(operator);
operands.add(builder.build());
traverse(right);
return;
}
}
// Otherwise, just write the node itself.
builder.visit(e);
}
traverse(node);
operands.add(builder.build());
return InfixPiece(operands);
}
/// Creates a [ListPiece] for the given bracket-delimited set of elements.
Piece createList(Iterable<AstNode> elements,
{Token? leftBracket,
Token? rightBracket,
ListStyle style = const ListStyle()}) {
var builder = DelimitedListBuilder(this, style);
if (leftBracket != null) builder.leftBracket(leftBracket);
elements.forEach(builder.visit);
if (rightBracket != null) builder.rightBracket(rightBracket);
return builder.build();
}
/// Creates a class, enum, extension, mixin, or mixin application class
/// 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.
Piece createType(NodeList<Annotation> metadata, List<Token?> modifiers,
Token keyword, Token? name,
{TypeParameterList? typeParameters,
Token? equals,
NamedType? superclass,
ExtendsClause? extendsClause,
OnClause? onClause,
WithClause? withClause,
ImplementsClause? implementsClause,
NativeClause? nativeClause,
(Token, TypeAnnotation)? onType,
({Token leftBracket, List<AstNode> members, Token rightBracket})? body,
Token? semicolon}) {
if (metadata.isNotEmpty) throw UnimplementedError('Type metadata.');
var header = buildPiece((b) {
modifiers.forEach(b.modifier);
b.token(keyword);
b.token(name, spaceBefore: true);
if (typeParameters != null) {
b.visit(typeParameters);
}
// Mixin application classes have ` = Superclass` after the declaration
// name.
if (equals != null) {
b.space();
b.token(equals);
b.space();
b.visit(superclass!);
}
});
var clauses = <ClausePiece>[];
void typeClause(Token keyword, List<AstNode> types) {
var keywordPiece = tokenPiece(keyword);
var typePieces = <Piece>[];
for (var type in types) {
typePieces.add(nodePiece(type, commaAfter: true));
}
clauses.add(ClausePiece(keywordPiece, typePieces));
}
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]);
}
ClausesPiece? clausesPiece;
if (clauses.isNotEmpty) {
clausesPiece = ClausesPiece(clauses,
allowLeadingClause: extendsClause != null || onClause != null);
}
Piece bodyPiece;
if (body != null) {
bodyPiece = createBody(body.leftBracket, body.members, body.rightBracket);
} else {
bodyPiece = tokenPiece(semicolon!);
}
return TypePiece(header, clausesPiece, bodyPiece, hasBody: body != null);
}
/// Creates a [ListPiece] for a type argument or type parameter list.
Piece createTypeList(
Token leftBracket, Iterable<AstNode> elements, Token rightBracket) {
return createList(
leftBracket: leftBracket,
elements,
rightBracket: rightBracket,
style: const ListStyle(commas: Commas.nonTrailing, splitCost: 3));
}
/// Writes the parts of a formal parameter shared by all formal parameter
/// types: metadata, `covariant`, etc.
void startFormalParameter(
FormalParameter parameter, AdjacentBuilder builder) {
if (parameter.metadata.isNotEmpty) throw UnimplementedError();
builder.modifier(parameter.requiredKeyword);
builder.modifier(parameter.covariantKeyword);
}
/// Handles the `async`, `sync*`, or `async*` modifiers on a function body.
void functionBodyModifiers(FunctionBody body, AdjacentBuilder builder) {
// The `async` or `sync` keyword.
builder.token(body.keyword);
builder.token(body.star);
if (body.keyword != null) builder.space();
}
/// Creates 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 (`:`)
/// * For-in loop iterator (`in`)
///
/// If [splitBeforeOperator] is `true`, then puts [operator] at the beginning
/// of the next line when it splits. Otherwise, puts the operator at the end
/// of the preceding line.
///
/// If [allowInnerSplit] is `true`, then a newline inside the target or
/// right-hand side doesn't force splitting at the operator itself.
Piece createAssignment(
AstNode target, Token operator, Expression rightHandSide,
{bool splitBeforeOperator = false,
bool includeComma = false,
bool spaceBeforeOperator = true,
bool allowInnerSplit = false}) {
// If the right-hand side can have block formatting, then a newline in
// it doesn't force the operator to split, as in:
//
// var list = [
// element,
// ];
allowInnerSplit |= rightHandSide.canBlockSplit;
if (splitBeforeOperator) {
var targetPiece = nodePiece(target);
var initializer = buildPiece((b) {
b.token(operator);
b.space();
b.visit(rightHandSide, commaAfter: includeComma);
});
return AssignPiece(targetPiece, initializer,
allowInnerSplit: allowInnerSplit);
} else {
var targetPiece = buildPiece((b) {
b.visit(target);
b.token(operator, spaceBefore: spaceBeforeOperator);
});
var initializer = nodePiece(rightHandSide, commaAfter: includeComma);
return AssignPiece(targetPiece, initializer,
allowInnerSplit: allowInnerSplit);
}
}
/// Invokes [buildCallback] with a new [AdjacentBuilder] and returns the
/// built result.
Piece buildPiece(void Function(AdjacentBuilder) buildCallback) {
var builder = AdjacentBuilder(this);
buildCallback(builder);
return builder.build();
}
/// Creates a piece for only [token].
///
/// If [lexeme] is given, uses that for the token's lexeme instead of its own.
///
/// If [commaAfter] is `true`, will look for and write a comma following the
/// token if there is one.
Piece tokenPiece(Token token, {String? lexeme, bool commaAfter = false}) {
return pieces.tokenPiece(token, lexeme: lexeme, commaAfter: commaAfter);
}
}