| // 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/adjacent.dart'; |
| import '../piece/assign.dart'; |
| import '../piece/clause.dart'; |
| import '../piece/control_flow.dart'; |
| import '../piece/for.dart'; |
| import '../piece/if_case.dart'; |
| import '../piece/infix.dart'; |
| import '../piece/list.dart'; |
| import '../piece/piece.dart'; |
| import '../piece/sequence.dart'; |
| import '../piece/type.dart'; |
| import '../piece/variable.dart'; |
| import 'ast_node_visitor.dart'; |
| import 'chain_builder.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); |
| |
| /// 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 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 { |
| /// A stack that handles forcing nested list, map, and set literals to split. |
| /// |
| /// Each entry corresponds to a collection currently being visited and the |
| /// value is whether or not it should be forced to split because an inner |
| /// collection was found inside it. |
| /// |
| /// When we begin a collection, we set all of the existing elements to `true` |
| /// then push `false` for the new collection. When done visiting the elements, |
| /// we pop the last value, If it's `true`, we know we visited a nested |
| /// collection so we force this one to split. |
| final List<bool> _collectionSplits = []; |
| |
| PieceWriter get pieces; |
| |
| CommentWriter get comments; |
| |
| NodeContext get parentContext; |
| |
| void visitNode(AstNode node, NodeContext context); |
| |
| /// Writes a [ListPiece] for an argument list. |
| void writeArgumentList( |
| Token leftBracket, List<AstNode> elements, Token rightBracket) { |
| writeList( |
| leftBracket: leftBracket, |
| elements, |
| rightBracket: rightBracket, |
| style: const ListStyle(allowBlockElement: true)); |
| } |
| |
| /// 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 [splitOnNestedCollection] is `true`, then this collection is forced to |
| /// split if it contains any non-empty collections where |
| /// [splitOnNestedCollection] is also `true`, even if the collection would |
| /// otherwise not need to split. This is `true` for list, map, and set |
| /// expressions because they are often used for composite data structures and |
| /// they're easier to read if they don't get packed too densely: |
| /// |
| /// // 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 aggregations of data where the fields are |
| /// more "closely" bundled together. Record expressions are sort of like |
| /// constructor invocations for an anonymous constructor. |
| /// |
| /// 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 splitOnNestedCollection = false, |
| bool preserveNewlines = false, |
| }) { |
| pieces.modifier(constKeyword); |
| pieces.visit(typeArguments); |
| |
| // If the list is completely empty, write the brackets inline so we create |
| // fewer pieces. |
| if (!elements.canSplit(rightBracket)) { |
| pieces.token(leftBracket); |
| pieces.token(rightBracket); |
| return; |
| } |
| |
| if (splitOnNestedCollection) { |
| // If this collection isn't empty, force all of the surrounding |
| // collections to split if they care to. |
| if (elements.isNotEmpty) { |
| _collectionSplits.fillRange(0, _collectionSplits.length, true); |
| } |
| |
| // Add this collection to the stack. |
| _collectionSplits.add(false); |
| } |
| |
| var collection = pieces.build(() { |
| writeList( |
| leftBracket: leftBracket, |
| elements, |
| rightBracket: rightBracket, |
| style: style, |
| preserveNewlines: preserveNewlines, |
| ); |
| }); |
| |
| // If there is a collection inside this one, force this one to split. |
| if (splitOnNestedCollection) { |
| if (_collectionSplits.removeLast()) 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 the constant is in an enum declaration that also declares members, then |
| /// [semicolon] should be the `;` token before the members, and |
| /// [isLastConstant] is `true` if [node] is the last constant before the |
| /// members. |
| Piece createEnumConstant(EnumConstantDeclaration node, |
| {bool isLastConstant = 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 (semicolon != null) { |
| if (!isLastConstant) { |
| pieces.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. |
| 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); |
| partsList.add(createCommaSeparated(forParts.updaters)); |
| } |
| |
| partsList.rightBracket(rightParenthesis); |
| forPartsPiece = partsList.build(); |
| |
| 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 `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(variable, forEachParts.inKeyword, forEachParts.iterable); |
| pieces.token(rightParenthesis); |
| }); |
| |
| case ForEachParts forEachParts && |
| ForEachPartsWithPattern(:var keyword, :var metadata, :var pattern): |
| forPartsPiece = pieces.build(() { |
| pieces.token(leftParenthesis); |
| |
| // Use a nested piece so that the metadata precedes the keyword and |
| // not the `(`. |
| pieces.withMetadata(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; |
| } |
| |
| var returnTypePiece = pieces.build(() { |
| for (var keyword in modifiers) { |
| pieces.modifier(keyword); |
| } |
| |
| pieces.visit(returnType); |
| }); |
| |
| var signature = pieces.build(() { |
| writeFunction(); |
| }); |
| |
| pieces.add(VariablePiece(returnTypePiece, [signature], hasType: true)); |
| } |
| |
| /// 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 (separator.type == TokenType.EQ) pieces.space(); |
| pieces.token(separator); |
| if (separator.type != TokenType.EQ) pieces.space(); |
| }); |
| |
| var valuePiece = nodePiece(value, context: NodeContext.assignment); |
| |
| pieces.add(AssignPiece( |
| left: parameter, |
| operatorPiece, |
| valuePiece, |
| canBlockSplitRight: value.canBlockSplit)); |
| } |
| |
| /// 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?>[]; |
| |
| // TODO(rnystrom): It would be good if the AssignPiece created for the |
| // default value could treat the parameter list on the left-hand side as |
| // block-splittable. But since it's a FunctionPiece and not directly a |
| // ListPiece, AssignPiece doesn't support block-splitting it. If #1466 is |
| // fixed, that may enable us to handle block-splitting here too. In |
| // practice, it doesn't really matter since function-typed formals are |
| // deprecated, default values on function-typed parameters are rare, and |
| // when both occur, they rarely split. |
| // 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 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); |
| 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); |
| }); |
| |
| // Include any `if` clauses. |
| var clauses = <Piece>[]; |
| 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(const [], [ |
| tokenPiece(combinatorNode.keyword), |
| for (var name in names) tokenPiece(name.token, commaAfter: true), |
| ])); |
| default: |
| throw StateError('Unknown combinator type $combinatorNode.'); |
| } |
| } |
| |
| // 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(tall): 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); |
| 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}) { |
| // Hoist any comments before the first operand so they don't force the |
| // infix operator to split. |
| var leadingComments = pieces.takeCommentsBefore(left.firstNonCommentToken); |
| |
| 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(leadingComments, [leftPiece, rightPiece])); |
| } |
| |
| /// 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}) { |
| // Hoist any comments before the first operand so they don't force the |
| // infix operator to split. |
| var leadingComments = pieces.takeCommentsBefore(node.firstNonCommentToken); |
| |
| 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(leadingComments, operands, indent: indent)); |
| } |
| |
| /// 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}) { |
| // 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 { |
| elements.forEach(builder.visit); |
| } |
| |
| builder.rightBracket(rightBracket); |
| pieces.add(builder.build()); |
| } |
| |
| /// 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.add(lineBuilder.build()); |
| 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; |
| } |
| |
| if (!atLineStart) builder.add(lineBuilder.build()); |
| } |
| |
| /// 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)); |
| } |
| |
| /// 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? node, {bool space = false}) { |
| pieces.token(operator, spaceAfter: space); |
| pieces.visit(node); |
| } |
| |
| /// 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(const [], [ |
| tokenPiece(keyword), |
| for (var type in types) nodePiece(type, commaAfter: true), |
| ])); |
| } |
| |
| 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)); |
| } |
| |
| /// 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 (`:`) |
| /// |
| /// If [canBlockSplitLeft] is `true`, then the left-hand operand supports |
| /// being block-formatted without indenting it farther, like: |
| /// |
| /// var [ |
| /// element, |
| /// ] = list; |
| void writeAssignment( |
| AstNode leftHandSide, Token operator, AstNode rightHandSide, |
| {bool includeComma = false, |
| bool canBlockSplitLeft = 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, |
| // ]; |
| canBlockSplitLeft |= switch (leftHandSide) { |
| 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(AssignPiece( |
| left: leftPiece, |
| operatorPiece, |
| rightPiece, |
| canBlockSplitLeft: canBlockSplitLeft, |
| canBlockSplitRight: canBlockSplitRight)); |
| } |
| |
| /// Writes a [Piece] for the `<variable> in <expression>` part of a for-in |
| /// loop. |
| void writeForIn(AstNode leftHandSide, Token inKeyword, Expression sequence) { |
| var leftPiece = |
| nodePiece(leftHandSide, context: NodeContext.forLoopVariable); |
| |
| var sequencePiece = pieces.build(() { |
| // Put the `in` at the beginning of the sequence. |
| pieces.token(inKeyword); |
| pieces.space(); |
| pieces.visit(sequence); |
| }); |
| |
| pieces.add(ForInPiece(leftPiece, sequencePiece, |
| canBlockSplitSequence: sequence.canBlockSplit)); |
| } |
| |
| /// 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); |
| } 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); |
| } |
| } |