| // 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 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:analyzer/source/line_info.dart'; |
| |
| import '../ast_extensions.dart'; |
| import '../back_end/code_writer.dart'; |
| import '../piece/assign.dart'; |
| import '../piece/assign_3_dot_7.dart'; |
| import '../piece/case.dart'; |
| import '../piece/constructor.dart'; |
| import '../piece/control_flow.dart'; |
| import '../piece/grouping.dart'; |
| import '../piece/infix.dart'; |
| import '../piece/leading_comment.dart'; |
| import '../piece/list.dart'; |
| import '../piece/piece.dart'; |
| import '../piece/type.dart'; |
| import '../piece/type_parameter_bound.dart'; |
| import '../piece/variable.dart'; |
| import '../profile.dart'; |
| import '../source_code.dart'; |
| import 'chain_builder.dart'; |
| import 'comment_writer.dart'; |
| import 'delimited_list_builder.dart'; |
| import 'formatting_style.dart'; |
| import 'piece_factory.dart'; |
| import 'piece_writer.dart'; |
| import 'sequence_builder.dart'; |
| |
| /// Visits every token of the AST and produces a tree of [Piece]s that |
| /// corresponds to it and contains every token and comment in the original |
| /// source. |
| /// |
| /// To avoid this class becoming a monolith, functionality is divided into a |
| /// couple of mixins, one for each area of functionality. This class then |
| /// contains only shared state and the visitor methods for the AST. |
| final class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory { |
| @override |
| final FormattingStyle style; |
| |
| @override |
| final PieceWriter pieces; |
| |
| @override |
| final CommentWriter comments; |
| |
| /// The context set by the surrounding AstNode when visiting a child, or |
| /// [NodeContext.none] if the parent node doesn't set a context. |
| @override |
| NodeContext get parentContext => _parentContext; |
| NodeContext _parentContext = NodeContext.none; |
| |
| /// Create a new visitor that will be called to visit the code in [source]. |
| factory AstNodeVisitor( |
| FormattingStyle style, |
| LineInfo lineInfo, |
| SourceCode source, |
| ) { |
| var comments = CommentWriter(lineInfo); |
| var pieces = PieceWriter(source, comments); |
| return AstNodeVisitor._(style, pieces, comments); |
| } |
| |
| AstNodeVisitor._(this.style, this.pieces, this.comments) { |
| pieces.bindVisitor(this); |
| } |
| |
| /// Visits [node] and returns the formatted result. |
| /// |
| /// Returns a [SourceCode] containing the resulting formatted source and |
| /// updated selection, if any. |
| /// |
| /// This is the only method that should be called externally. Everything else |
| /// is effectively private. |
| SourceCode run(SourceCode source, AstNode node) { |
| Profile.begin('AstNodeVisitor.run()'); |
| |
| Profile.begin('AstNodeVisitor build Piece tree'); |
| |
| // Always treat the code being formatted as contained in a sequence, even |
| // if we aren't formatting an entire compilation unit. That way, comments |
| // before and after the node are handled properly. |
| var sequence = SequenceBuilder(this); |
| |
| if (node is CompilationUnit) { |
| if (node.scriptTag case var scriptTag?) { |
| sequence.visit(scriptTag); |
| sequence.addBlank(); |
| } |
| |
| // Put a blank line between the library tag and the other directives. |
| Iterable<Directive> directives = node.directives; |
| if (directives.isNotEmpty && directives.first is LibraryDirective) { |
| sequence.visit(directives.first); |
| sequence.addBlank(); |
| directives = directives.skip(1); |
| } |
| |
| for (var directive in directives) { |
| sequence.visit(directive); |
| } |
| |
| // Add a blank line between directives and declarations. |
| sequence.addBlank(); |
| |
| for (var declaration in node.declarations) { |
| var hasBody = |
| declaration is ClassDeclaration || |
| declaration is EnumDeclaration || |
| declaration is ExtensionDeclaration; |
| |
| // Add a blank line before types with bodies. |
| if (hasBody) sequence.addBlank(); |
| |
| sequence.visit(declaration); |
| |
| // Add a blank line after type or function declarations with bodies. |
| if (hasBody || declaration.hasNonEmptyBody) sequence.addBlank(); |
| } |
| } else { |
| // Just formatting a single statement. |
| sequence.visit(node); |
| } |
| |
| // Write any comments at the end of the code. |
| sequence.addCommentsBefore(node.endToken.next!); |
| |
| var unitPiece = sequence.build(); |
| |
| Profile.end('AstNodeVisitor build Piece tree'); |
| |
| // Finish writing and return the complete result. |
| var result = pieces.finish(style, source, unitPiece); |
| |
| Profile.end('AstNodeVisitor.run()'); |
| |
| return result; |
| } |
| |
| @override |
| void visitAdjacentStrings(AdjacentStrings node) { |
| var indent = style.is3Dot7 ? node.indentStrings3Dot7 : node.indentStrings; |
| var piece = InfixPiece( |
| node.strings.map(nodePiece).toList(), |
| indent: indent ? Indent.infix : Indent.none, |
| is3Dot7: style.is3Dot7, |
| ); |
| |
| // Adjacent strings always split. |
| piece.pin(State.split); |
| |
| pieces.add(piece); |
| } |
| |
| @override |
| void visitAnnotation(Annotation node) { |
| pieces.token(node.atSign); |
| pieces.visit(node.name); |
| pieces.visit(node.typeArguments); |
| pieces.token(node.period); |
| pieces.visit(node.constructorName); |
| pieces.visit(node.arguments); |
| } |
| |
| @override |
| void visitArgumentList(ArgumentList node) { |
| writeArgumentList(node); |
| } |
| |
| @override |
| void visitAsExpression(AsExpression node) { |
| writeInfix( |
| node.expression, |
| node.asOperator, |
| node.type, |
| // Don't use Indent.infix after 3.7 because it will flatten the |
| // indentation if the expression occurs in an assignment. |
| // TODO(rnystrom): We probably do want to format `as` the same as other |
| // binary operators, but the tall style already treats them differently, |
| // so leaving that alone for now. |
| indent: style.is3Dot7 ? Indent.infix : Indent.expression, |
| ); |
| } |
| |
| @override |
| void visitAssertInitializer(AssertInitializer node) { |
| pieces.token(node.assertKeyword); |
| writeArguments(node.leftParenthesis, [ |
| node.condition, |
| if (node.message case var message?) message, |
| ], node.rightParenthesis); |
| } |
| |
| @override |
| void visitAssertStatement(AssertStatement node) { |
| pieces.token(node.assertKeyword); |
| writeArguments(node.leftParenthesis, [ |
| node.condition, |
| if (node.message case var message?) message, |
| ], node.rightParenthesis); |
| pieces.token(node.semicolon); |
| } |
| |
| @override |
| void visitAssignedVariablePattern(AssignedVariablePattern node) { |
| pieces.token(node.name); |
| } |
| |
| @override |
| void visitAssignmentExpression(AssignmentExpression node) { |
| writeAssignment(node.leftHandSide, node.operator, node.rightHandSide); |
| } |
| |
| @override |
| void visitAwaitExpression(AwaitExpression node) { |
| writePrefix(node.awaitKeyword, space: true, node.expression); |
| } |
| |
| @override |
| void visitBinaryExpression(BinaryExpression node) { |
| // In 3.7 style, flatten binary operands in assignment contexts. (In 3.8 |
| // and later, indentation merging accomplishes the same thing.) |
| var indent = !style.is3Dot7 || _parentContext != NodeContext.assignment; |
| |
| writeInfixChain<BinaryExpression>( |
| node, |
| precedence: node.operator.type.precedence, |
| indent: indent, |
| (expression) => ( |
| expression.leftOperand, |
| expression.operator, |
| expression.rightOperand, |
| ), |
| ); |
| } |
| |
| @override |
| void visitBlock(Block node) { |
| writeBlock(node); |
| } |
| |
| @override |
| void visitBlockFunctionBody(BlockFunctionBody node) { |
| pieces.space(); |
| writeFunctionBodyModifiers(node); |
| pieces.visit(node.block); |
| } |
| |
| @override |
| void visitBooleanLiteral(BooleanLiteral node) { |
| pieces.token(node.literal); |
| } |
| |
| @override |
| void visitBreakStatement(BreakStatement node) { |
| writeBreak(node.breakKeyword, node.label, node.semicolon); |
| } |
| |
| @override |
| void visitCascadeExpression(CascadeExpression node) { |
| pieces.add(ChainBuilder(this, node).buildCascade()); |
| } |
| |
| @override |
| void visitCastPattern(CastPattern node) { |
| writeInfix(node.pattern, node.asToken, node.type); |
| } |
| |
| @override |
| void visitCatchClause(CatchClause node) { |
| throw UnsupportedError('This node is handled by visitTryStatement().'); |
| } |
| |
| @override |
| void visitCatchClauseParameter(CatchClauseParameter node) { |
| pieces.token(node.name); |
| } |
| |
| @override |
| void visitClassDeclaration(ClassDeclaration node) { |
| writeType( |
| node.metadata, |
| [ |
| node.abstractKeyword, |
| node.baseKeyword, |
| node.interfaceKeyword, |
| node.finalKeyword, |
| node.sealedKeyword, |
| node.mixinKeyword, |
| node.classKeyword, |
| ], |
| node.name, |
| typeParameters: node.typeParameters, |
| extendsClause: node.extendsClause, |
| withClause: node.withClause, |
| implementsClause: node.implementsClause, |
| nativeClause: node.nativeClause, |
| body: () { |
| return pieces.build(() { |
| writeBody(node.leftBracket, node.members, node.rightBracket); |
| }); |
| }, |
| ); |
| } |
| |
| @override |
| void visitClassTypeAlias(ClassTypeAlias node) { |
| writeType( |
| node.metadata, |
| [ |
| node.abstractKeyword, |
| node.baseKeyword, |
| node.interfaceKeyword, |
| node.finalKeyword, |
| node.sealedKeyword, |
| node.mixinKeyword, |
| node.typedefKeyword, |
| ], |
| node.name, |
| equals: node.equals, |
| superclass: node.superclass, |
| typeParameters: node.typeParameters, |
| withClause: node.withClause, |
| implementsClause: node.implementsClause, |
| bodyType: TypeBodyType.semicolon, |
| body: () => tokenPiece(node.semicolon), |
| ); |
| } |
| |
| @override |
| void visitComment(Comment node) { |
| throw UnsupportedError('Comments should be handled elsewhere.'); |
| } |
| |
| @override |
| void visitCommentReference(CommentReference node) { |
| throw UnsupportedError('Comments should be handled elsewhere.'); |
| } |
| |
| @override |
| void visitCompilationUnit(CompilationUnit node) { |
| throw UnsupportedError( |
| 'CompilationUnit should be handled directly by run().', |
| ); |
| } |
| |
| @override |
| void visitConditionalExpression(ConditionalExpression node) { |
| // Flatten a series of else-if-like chained conditionals into a single long |
| // infix piece. This produces a flattened style like: |
| // |
| // condition |
| // ? thenBranch |
| // : condition2 |
| // ? thenBranch2 |
| // : elseBranch; |
| // |
| // This (arguably) looks nicer. More importantly, it means that all but the |
| // last operand can be formatted separately, which is important to avoid |
| // pathological performance in the solved with long nested conditional |
| // chains. |
| var operands = [nodePiece(node.condition)]; |
| |
| void addOperand(Token operator, Expression operand) { |
| if (!style.is3Dot7) { |
| // If there are comments before a branch, then hoist them so they aren't |
| // indented with the branch body. |
| operands.addAll(pieces.takeCommentsBefore(operator)); |
| } |
| |
| operands.add( |
| pieces.build(() { |
| pieces.token(operator); |
| pieces.space(); |
| pieces.visit(operand, context: NodeContext.conditionalBranch); |
| }), |
| ); |
| } |
| |
| var conditional = node; |
| while (true) { |
| addOperand(conditional.question, conditional.thenExpression); |
| |
| var elseBranch = conditional.elseExpression; |
| if (elseBranch is ConditionalExpression) { |
| addOperand(conditional.colon, elseBranch.condition); |
| conditional = elseBranch; |
| } else { |
| addOperand(conditional.colon, conditional.elseExpression); |
| break; |
| } |
| } |
| |
| var piece = InfixPiece.conditional(operands, is3Dot7: style.is3Dot7); |
| |
| // If conditional expressions are directly nested, force them all to split, |
| // both parents and children. |
| if (_parentContext == NodeContext.conditionalBranch || |
| node.thenExpression is ConditionalExpression || |
| node.elseExpression is ConditionalExpression) { |
| piece.pin(State.split); |
| } |
| |
| pieces.add(piece); |
| } |
| |
| @override |
| void visitConfiguration(Configuration node) { |
| pieces.token(node.ifKeyword); |
| pieces.space(); |
| pieces.token(node.leftParenthesis); |
| |
| if (node.equalToken case var equals?) { |
| // Hoist comments so that they don't force the `==` to split. |
| pieces.hoistLeadingComments(node.name.firstNonCommentToken, () { |
| return InfixPiece(is3Dot7: style.is3Dot7, [ |
| pieces.build(() { |
| pieces.visit(node.name); |
| pieces.space(); |
| pieces.token(equals); |
| }), |
| nodePiece(node.value!), |
| ]); |
| }); |
| } else { |
| pieces.visit(node.name); |
| } |
| |
| pieces.token(node.rightParenthesis); |
| pieces.space(); |
| pieces.visit(node.uri); |
| } |
| |
| @override |
| void visitConstantPattern(ConstantPattern node) { |
| if (style.is3Dot7) { |
| writePrefix(node.constKeyword, space: true, node.expression); |
| } else { |
| if (node.constKeyword case var constKeyword?) { |
| pieces.token(constKeyword, spaceAfter: true); |
| pieces.visit(node.expression); |
| } else { |
| pieces.visit(node.expression); |
| } |
| } |
| } |
| |
| @override |
| void visitConstructorDeclaration(ConstructorDeclaration node) { |
| pieces.withMetadata(node.metadata, () { |
| var header = pieces.build(() { |
| pieces.modifier(node.externalKeyword); |
| pieces.modifier(node.constKeyword); |
| pieces.modifier(node.factoryKeyword); |
| pieces.visit(node.returnType); |
| pieces.token(node.period); |
| pieces.token(node.name); |
| }); |
| |
| var parameters = nodePiece(node.parameters); |
| |
| Piece? redirect; |
| Piece? initializerSeparator; |
| Piece? initializers; |
| if (node.redirectedConstructor case var constructor?) { |
| var separator = pieces.build(() { |
| pieces.token(node.separator); |
| pieces.space(); |
| }); |
| |
| if (style.is3Dot7) { |
| redirect = AssignPiece3Dot7( |
| separator, |
| nodePiece(constructor, context: NodeContext.assignment), |
| canBlockSplitRight: false, |
| ); |
| } else { |
| redirect = AssignPiece(separator, nodePiece(constructor)); |
| } |
| } else if (node.initializers.isNotEmpty) { |
| initializerSeparator = tokenPiece(node.separator!); |
| initializers = createCommaSeparated(node.initializers); |
| } |
| |
| var body = nodePiece(node.body); |
| |
| pieces.add( |
| ConstructorPiece( |
| header, |
| parameters, |
| body, |
| canSplitParameters: node.parameters.parameters.canSplit( |
| node.parameters.rightParenthesis, |
| ), |
| hasOptionalParameter: node.parameters.rightDelimiter != null, |
| redirect: redirect, |
| initializerSeparator: initializerSeparator, |
| initializers: initializers, |
| ), |
| ); |
| }); |
| } |
| |
| @override |
| void visitConstructorFieldInitializer(ConstructorFieldInitializer node) { |
| pieces.token(node.thisKeyword); |
| pieces.token(node.period); |
| writeAssignment(node.fieldName, node.equals, node.expression); |
| } |
| |
| @override |
| void visitConstructorName(ConstructorName node) { |
| if (node.type.importPrefix case var importPrefix?) { |
| pieces.token(importPrefix.name); |
| pieces.token(importPrefix.period); |
| } |
| |
| // The name of the type being constructed. |
| var type = node.type; |
| pieces.token(type.name); |
| pieces.visit(type.typeArguments); |
| pieces.token(type.question); |
| |
| // If this is a named constructor, the name. |
| if (node.name != null) { |
| pieces.token(node.period); |
| pieces.visit(node.name); |
| } |
| } |
| |
| @override |
| void visitConstructorSelector(ConstructorSelector node) { |
| pieces.token(node.period); |
| pieces.visit(node.name); |
| } |
| |
| @override |
| void visitContinueStatement(ContinueStatement node) { |
| writeBreak(node.continueKeyword, node.label, node.semicolon); |
| } |
| |
| @override |
| void visitDeclaredIdentifier(DeclaredIdentifier node) { |
| writeParameter( |
| metadata: node.metadata, |
| modifiers: [node.keyword], |
| node.type, |
| node.name, |
| ); |
| } |
| |
| @override |
| void visitDeclaredVariablePattern(DeclaredVariablePattern node) { |
| writePatternVariable(node.keyword, node.type, node.name); |
| } |
| |
| @override |
| void visitDefaultFormalParameter(DefaultFormalParameter node) { |
| // Visit the inner parameter. It will then access its parent to extract the |
| // default value. |
| pieces.visit(node.parameter); |
| } |
| |
| @override |
| void visitDoStatement(DoStatement node) { |
| pieces.token(node.doKeyword); |
| pieces.space(); |
| pieces.visit(node.body); |
| pieces.space(); |
| pieces.token(node.whileKeyword); |
| pieces.space(); |
| pieces.token(node.leftParenthesis); |
| pieces.visit(node.condition); |
| pieces.token(node.rightParenthesis); |
| pieces.token(node.semicolon); |
| } |
| |
| @override |
| void visitDotShorthandConstructorInvocation( |
| DotShorthandConstructorInvocation node, |
| ) { |
| pieces.modifier(node.constKeyword); |
| pieces.token(node.period); |
| pieces.visit(node.constructorName); |
| pieces.visit(node.typeArguments); |
| pieces.visit(node.argumentList); |
| } |
| |
| @override |
| void visitDotShorthandInvocation(DotShorthandInvocation node) { |
| pieces.token(node.period); |
| pieces.visit(node.memberName); |
| pieces.visit(node.typeArguments); |
| pieces.visit(node.argumentList); |
| } |
| |
| @override |
| void visitDotShorthandPropertyAccess(DotShorthandPropertyAccess node) { |
| pieces.token(node.period); |
| pieces.visit(node.propertyName); |
| } |
| |
| @override |
| void visitDottedName(DottedName node) { |
| writeDotted(node.components); |
| } |
| |
| @override |
| void visitDoubleLiteral(DoubleLiteral node) { |
| pieces.token(node.literal); |
| } |
| |
| @override |
| void visitEmptyFunctionBody(EmptyFunctionBody node) { |
| pieces.token(node.semicolon); |
| } |
| |
| @override |
| void visitEmptyStatement(EmptyStatement node) { |
| pieces.token(node.semicolon); |
| } |
| |
| @override |
| void visitEnumConstantDeclaration(EnumConstantDeclaration node) { |
| pieces.add(createEnumConstant(node)); |
| } |
| |
| @override |
| void visitEnumDeclaration(EnumDeclaration node) { |
| writeType( |
| node.metadata, |
| [node.enumKeyword], |
| node.name, |
| typeParameters: node.typeParameters, |
| withClause: node.withClause, |
| implementsClause: node.implementsClause, |
| bodyType: node.members.isEmpty ? TypeBodyType.list : TypeBodyType.block, |
| body: () { |
| if (node.members.isEmpty) { |
| // If there are no members, format the constants like a list. This |
| // keeps the enum declaration on one line if it fits. |
| var builder = DelimitedListBuilder( |
| this, |
| const ListStyle(spaceWhenUnsplit: true), |
| ); |
| |
| builder.leftBracket(node.leftBracket); |
| node.constants.forEach(builder.visit); |
| builder.rightBracket(semicolon: node.semicolon, node.rightBracket); |
| return builder.build( |
| forceSplit: style.preserveTrailingCommaBefore( |
| node.semicolon ?? node.rightBracket, |
| ), |
| ); |
| } else { |
| // If there are members, format it like a block where each constant |
| // and member is on its own line. |
| var builder = SequenceBuilder(this); |
| builder.leftBracket(node.leftBracket); |
| |
| // In 3.10 and later, preserved trailing commas will also preserve a |
| // trailing comma in an enum with members. That in turn forces the |
| // `;` onto its own line after the last costant. Prior to 3.10, the |
| // behavior is the same as when preserved trailing commas is off |
| // where the last constant's comma is removed and the `;` is placed |
| // there instead. |
| for (var constant in node.constants) { |
| var isLast = constant == node.constants.last; |
| builder.addCommentsBefore(constant.firstNonCommentToken); |
| builder.add( |
| createEnumConstant( |
| constant, |
| commaAfter: |
| !isLast || style.preserveTrailingCommaAfterEnumValues, |
| semicolon: isLast ? node.semicolon : null, |
| ), |
| ); |
| } |
| |
| // If we are preserving the trailing comma, then put the `;` on its |
| // own line after the last constant. |
| if (style.preserveTrailingCommaAfterEnumValues) { |
| builder.add(tokenPiece(node.semicolon!)); |
| } |
| |
| // Insert a blank line between the constants and members. |
| builder.addBlank(); |
| |
| for (var node in node.members) { |
| builder.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) builder.addBlank(); |
| } |
| |
| builder.rightBracket(node.rightBracket); |
| return builder.build(); |
| } |
| }, |
| ); |
| } |
| |
| @override |
| void visitExportDirective(ExportDirective node) { |
| writeImport(node, node.exportKeyword); |
| } |
| |
| @override |
| void visitExpressionFunctionBody(ExpressionFunctionBody node) { |
| var operatorPiece = pieces.build(() { |
| pieces.space(); |
| writeFunctionBodyModifiers(node); |
| pieces.token(node.functionDefinition); |
| }); |
| |
| var expression = nodePiece( |
| node.expression, |
| context: NodeContext.assignment, |
| ); |
| |
| if (style.is3Dot7) { |
| pieces.add( |
| AssignPiece3Dot7( |
| operatorPiece, |
| expression, |
| canBlockSplitRight: node.expression.canBlockSplit, |
| avoidBlockSplitRight: |
| node.expression.blockFormatType == BlockFormat.invocation, |
| ), |
| ); |
| } else { |
| var assignPiece = AssignPiece( |
| operatorPiece, |
| expression, |
| // Prefer splitting at `=>` and keeping the expression together unless |
| // it's a collection literal. |
| avoidSplit: node.expression.isHomogeneousCollectionBody, |
| ); |
| |
| // If a `=>` is directly nested inside another, force the outer one to |
| // split. This is for performance reasons. A series of nested AssignPieces |
| // can have combinatorial performance otherwise. |
| // TODO(rnystrom): Figure out a better way to handle this. We could |
| // possibly collapse a series of curried function expressions into a |
| // single piece similar to how we handle nested conditional expressions. |
| // In practice, outside of a few libraries that lean heavily on currying, |
| // this is very rare. |
| if (node.expression case FunctionExpression( |
| body: ExpressionFunctionBody(), |
| )) { |
| assignPiece.pin(State.split); |
| } |
| |
| pieces.add(assignPiece); |
| } |
| |
| pieces.token(node.semicolon); |
| } |
| |
| @override |
| void visitExpressionStatement(ExpressionStatement node) { |
| pieces.visit(node.expression); |
| pieces.token(node.semicolon); |
| } |
| |
| @override |
| void visitExtendsClause(ExtendsClause node) { |
| throw UnsupportedError( |
| 'This node is handled by PieceFactory.createType().', |
| ); |
| } |
| |
| @override |
| void visitExtensionDeclaration(ExtensionDeclaration node) { |
| (Token, TypeAnnotation)? onType; |
| if (node.onClause case var onClause?) { |
| onType = (onClause.onKeyword, onClause.extendedType); |
| } |
| |
| writeType( |
| node.metadata, |
| [node.extensionKeyword], |
| node.name, |
| typeParameters: node.typeParameters, |
| onType: onType, |
| body: () { |
| return pieces.build(() { |
| writeBody(node.leftBracket, node.members, node.rightBracket); |
| }); |
| }, |
| ); |
| } |
| |
| @override |
| void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) { |
| writeType( |
| node.metadata, |
| [ |
| node.extensionKeyword, |
| node.typeKeyword, |
| if (node.constKeyword case var keyword?) keyword, |
| ], |
| node.name, |
| typeParameters: node.typeParameters, |
| representation: node.representation, |
| implementsClause: node.implementsClause, |
| body: () { |
| return pieces.build(() { |
| writeBody(node.leftBracket, node.members, node.rightBracket); |
| }); |
| }, |
| ); |
| } |
| |
| @override |
| void visitFieldDeclaration(FieldDeclaration node) { |
| pieces.withMetadata(node.metadata, () { |
| pieces.modifier(node.externalKeyword); |
| pieces.modifier(node.staticKeyword); |
| pieces.modifier(node.abstractKeyword); |
| pieces.modifier(node.covariantKeyword); |
| pieces.visit(node.fields); |
| pieces.token(node.semicolon); |
| }); |
| } |
| |
| @override |
| void visitFieldFormalParameter(FieldFormalParameter node) { |
| if (node.parameters case var parameters?) { |
| // A function-typed field formal like: |
| // |
| // C(this.fn(parameter)); |
| writeFunctionType( |
| node.type, |
| fieldKeyword: node.thisKeyword, |
| period: node.period, |
| node.name, |
| node.typeParameters, |
| parameters, |
| node.question, |
| parameter: node, |
| ); |
| } else { |
| writeFormalParameter( |
| node, |
| mutableKeyword: node.keyword, |
| fieldKeyword: node.thisKeyword, |
| period: node.period, |
| node.type, |
| node.name, |
| ); |
| } |
| } |
| |
| @override |
| void visitFormalParameterList(FormalParameterList node) { |
| // Find the first non-mandatory parameter (if there are any). |
| var firstOptional = node.parameters.indexWhere( |
| (p) => p is DefaultFormalParameter, |
| ); |
| |
| // If the parameter list is completely empty, write the brackets inline so |
| // that we generate fewer separate pieces. |
| if (!node.parameters.canSplit(node.rightParenthesis)) { |
| pieces.token(node.leftParenthesis); |
| pieces.token(node.rightParenthesis); |
| return; |
| } |
| |
| // If all parameters are optional, put the `[` or `{` right after `(`. |
| var builder = DelimitedListBuilder(this); |
| |
| builder.addLeftBracket( |
| pieces.build(() { |
| pieces.token(node.leftParenthesis); |
| if (node.parameters.isNotEmpty && firstOptional == 0) { |
| pieces.token(node.leftDelimiter); |
| } |
| }), |
| ); |
| |
| for (var i = 0; i < node.parameters.length; i++) { |
| // If this is the first optional parameter, put the delimiter before it. |
| if (firstOptional > 0 && i == firstOptional) { |
| builder.leftDelimiter(node.leftDelimiter!); |
| } |
| |
| builder.visit(node.parameters[i]); |
| } |
| |
| builder.rightBracket(node.rightParenthesis, delimiter: node.rightDelimiter); |
| pieces.add( |
| builder.build( |
| forceSplit: style.preserveTrailingCommaBefore( |
| node.rightDelimiter ?? node.rightParenthesis, |
| ), |
| blockShaped: false, |
| ), |
| ); |
| } |
| |
| @override |
| void visitForElement(ForElement node) { |
| writeFor( |
| awaitKeyword: node.awaitKeyword, |
| forKeyword: node.forKeyword, |
| leftParenthesis: node.leftParenthesis, |
| forLoopParts: node.forLoopParts, |
| rightParenthesis: node.rightParenthesis, |
| body: node.body, |
| hasBlockBody: node.body.isSpreadCollection, |
| forceSplitBody: node.body.isControlFlowElement, |
| ); |
| } |
| |
| @override |
| void visitForStatement(ForStatement node) { |
| writeFor( |
| awaitKeyword: node.awaitKeyword, |
| forKeyword: node.forKeyword, |
| leftParenthesis: node.leftParenthesis, |
| forLoopParts: node.forLoopParts, |
| rightParenthesis: node.rightParenthesis, |
| body: node.body, |
| hasBlockBody: node.body is Block, |
| ); |
| } |
| |
| @override |
| void visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) { |
| throw UnsupportedError('This node is handled by writeFor().'); |
| } |
| |
| @override |
| void visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) { |
| throw UnsupportedError('This node is handled by writeFor().'); |
| } |
| |
| @override |
| void visitForEachPartsWithPattern(ForEachPartsWithPattern node) { |
| throw UnsupportedError('This node is handled by writeFor().'); |
| } |
| |
| @override |
| void visitForPartsWithDeclarations(ForPartsWithDeclarations node) { |
| throw UnsupportedError('This node is handled by writeFor().'); |
| } |
| |
| @override |
| void visitForPartsWithExpression(ForPartsWithExpression node) { |
| throw UnsupportedError('This node is handled by writeFor().'); |
| } |
| |
| @override |
| void visitForPartsWithPattern(ForPartsWithPattern node) { |
| throw UnsupportedError('This node is handled by writeFor().'); |
| } |
| |
| @override |
| void visitFunctionDeclaration(FunctionDeclaration node) { |
| writeFunction( |
| metadata: node.metadata, |
| modifiers: [node.externalKeyword], |
| returnType: node.returnType, |
| propertyKeyword: node.propertyKeyword, |
| name: node.name, |
| typeParameters: node.functionExpression.typeParameters, |
| parameters: node.functionExpression.parameters, |
| body: node.functionExpression.body, |
| ); |
| } |
| |
| @override |
| void visitFunctionDeclarationStatement(FunctionDeclarationStatement node) { |
| pieces.add(nodePiece(node.functionDeclaration)); |
| } |
| |
| @override |
| void visitFunctionExpression(FunctionExpression node) { |
| writeFunction( |
| typeParameters: node.typeParameters, |
| parameters: node.parameters, |
| body: node.body, |
| ); |
| } |
| |
| @override |
| void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { |
| pieces.visit(node.function); |
| pieces.visit(node.typeArguments); |
| pieces.visit(node.argumentList); |
| } |
| |
| @override |
| void visitFunctionReference(FunctionReference node) { |
| pieces.visit(node.function); |
| pieces.visit(node.typeArguments); |
| } |
| |
| @override |
| void visitFunctionTypeAlias(FunctionTypeAlias node) { |
| pieces.withMetadata(node.metadata, () { |
| pieces.token(node.typedefKeyword); |
| pieces.space(); |
| pieces.visit(node.returnType, spaceAfter: true); |
| pieces.token(node.name); |
| pieces.visit(node.typeParameters); |
| pieces.visit(node.parameters); |
| pieces.token(node.semicolon); |
| }); |
| } |
| |
| @override |
| void visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) { |
| writeFunctionType( |
| parameter: node, |
| node.returnType, |
| node.name, |
| node.typeParameters, |
| node.parameters, |
| node.question, |
| ); |
| } |
| |
| @override |
| void visitGenericFunctionType(GenericFunctionType node) { |
| writeFunctionType( |
| node.returnType, |
| node.functionKeyword, |
| node.typeParameters, |
| node.parameters, |
| node.question, |
| ); |
| } |
| |
| @override |
| void visitGenericTypeAlias(GenericTypeAlias node) { |
| pieces.withMetadata(node.metadata, () { |
| pieces.token(node.typedefKeyword); |
| pieces.space(); |
| pieces.token(node.name); |
| pieces.visit(node.typeParameters); |
| pieces.space(); |
| if (style.is3Dot7) { |
| pieces.add( |
| AssignPiece3Dot7(tokenPiece(node.equals), nodePiece(node.type)), |
| ); |
| } else { |
| pieces.add(AssignPiece(tokenPiece(node.equals), nodePiece(node.type))); |
| } |
| pieces.token(node.semicolon); |
| }); |
| } |
| |
| @override |
| void visitHideCombinator(HideCombinator node) { |
| throw UnsupportedError('Combinators are handled by createImport().'); |
| } |
| |
| @override |
| void visitIfElement(IfElement node) { |
| var piece = ControlFlowPiece(isStatement: false); |
| |
| // Recurses through the else branches to flatten them into a linear if-else |
| // chain handled by a single [IfPiece]. |
| void traverse(Token? precedingElse, IfElement ifElement) { |
| var spreadThen = ifElement.thenElement.spreadCollection; |
| |
| var condition = pieces.build(() { |
| pieces.token(precedingElse, spaceAfter: true); |
| writeIfCondition( |
| ifElement.ifKeyword, |
| ifElement.leftParenthesis, |
| ifElement.expression, |
| ifElement.caseClause, |
| ifElement.rightParenthesis, |
| ); |
| |
| // Make the `...` part of the header so that IfPiece can correctly |
| // constrain the inner collection literal's ListPiece to split. |
| if (spreadThen != null) { |
| pieces.space(); |
| pieces.token(spreadThen.spreadOperator); |
| } |
| }); |
| |
| Piece thenElement; |
| if (spreadThen != null) { |
| thenElement = nodePiece(spreadThen.expression); |
| } else { |
| thenElement = nodePiece(ifElement.thenElement); |
| } |
| |
| // If the then branch of an if element is itself a control flow |
| // element, then force the outer if to always split. |
| if (ifElement.thenElement.isControlFlowElement) { |
| piece.pin(State.split); |
| } |
| |
| piece.add(condition, thenElement, isBlock: spreadThen != null); |
| |
| switch (ifElement.elseElement) { |
| case IfElement elseIf: |
| // Hit an else-if, so flatten it into the chain with the `else` |
| // becoming part of the next section's header. |
| traverse(ifElement.elseKeyword, elseIf); |
| |
| case var elseElement?: |
| var spreadElse = elseElement.spreadCollection; |
| |
| // Any other kind of else body ends the chain, with the header for |
| // the last section just being the `else` keyword. |
| var header = pieces.build(() { |
| pieces.token(ifElement.elseKeyword!); |
| |
| // Make the `...` part of the header so that IfPiece can correctly |
| // constrain the inner collection literal's ListPiece to split. |
| if (spreadElse != null) { |
| pieces.space(); |
| pieces.token(spreadElse.spreadOperator); |
| } |
| }); |
| |
| Piece statement; |
| if (spreadElse != null) { |
| statement = nodePiece(spreadElse.expression); |
| } else { |
| statement = nodePiece(elseElement); |
| } |
| |
| piece.add(header, statement, isBlock: spreadElse != null); |
| |
| // If the else branch of an if element is itself a control flow |
| // element, then force the outer if to always split. |
| if (ifElement.thenElement.isControlFlowElement) { |
| piece.pin(State.split); |
| } |
| |
| case null: |
| break; // Nothing to do. |
| } |
| } |
| |
| traverse(null, node); |
| pieces.add(piece); |
| } |
| |
| @override |
| void visitIfStatement(IfStatement node) { |
| var piece = ControlFlowPiece(); |
| |
| // Recurses through the else branches to flatten them into a linear if-else |
| // chain handled by a single [IfPiece]. |
| void traverse(Token? precedingElse, IfStatement ifStatement) { |
| var condition = pieces.build(() { |
| pieces.token(precedingElse, spaceAfter: true); |
| writeIfCondition( |
| ifStatement.ifKeyword, |
| ifStatement.leftParenthesis, |
| ifStatement.expression, |
| ifStatement.caseClause, |
| ifStatement.rightParenthesis, |
| ); |
| pieces.space(); |
| }); |
| |
| // Edge case: When the then branch is a block and there is an else clause |
| // after it, we want to force the block to split even if empty, like: |
| // |
| // if (condition) { |
| // } else { |
| // body; |
| // } |
| var thenStatement = switch (ifStatement.thenStatement) { |
| Block thenBlock when ifStatement.elseStatement != null => pieces.build( |
| () { |
| writeBlock(thenBlock, forceSplit: true); |
| }, |
| ), |
| _ => nodePiece(ifStatement.thenStatement), |
| }; |
| |
| piece.add( |
| condition, |
| thenStatement, |
| isBlock: ifStatement.thenStatement is Block, |
| ); |
| |
| switch (ifStatement.elseStatement) { |
| case IfStatement elseIf: |
| // Hit an else-if, so flatten it into the chain with the `else` |
| // becoming part of the next section's header. |
| traverse(ifStatement.elseKeyword, elseIf); |
| |
| case var elseStatement?: |
| // Any other kind of else body ends the chain, with the header for |
| // the last section just being the `else` keyword. |
| var header = pieces.build(() { |
| pieces.token(ifStatement.elseKeyword, spaceAfter: true); |
| }); |
| var statement = nodePiece(elseStatement); |
| piece.add(header, statement, isBlock: elseStatement is Block); |
| } |
| } |
| |
| traverse(null, node); |
| |
| // If statements almost always split at the clauses unless the if is a |
| // simple if with only a single unbraced then statement and no else clause, |
| // like: |
| // |
| // if (condition) print("ok"); |
| if (node.thenStatement is Block || node.elseStatement != null) { |
| piece.pin(State.split); |
| } |
| |
| pieces.add(piece); |
| } |
| |
| @override |
| void visitImplementsClause(ImplementsClause node) { |
| throw UnsupportedError( |
| 'This node is handled by PieceFactory.createType().', |
| ); |
| } |
| |
| @override |
| void visitImportDirective(ImportDirective node) { |
| writeImport( |
| node, |
| node.importKeyword, |
| deferredKeyword: node.deferredKeyword, |
| asKeyword: node.asKeyword, |
| prefix: node.prefix, |
| ); |
| } |
| |
| @override |
| void visitIndexExpression(IndexExpression node) { |
| pieces.visit(node.target); |
| writeIndexExpression(node); |
| } |
| |
| @override |
| void visitInstanceCreationExpression(InstanceCreationExpression node) { |
| pieces.token(node.keyword, spaceAfter: true); |
| |
| var constructor = node.constructorName; |
| if (constructor.type.importPrefix case var importPrefix?) { |
| pieces.token(importPrefix.name); |
| pieces.token(importPrefix.period); |
| } |
| |
| // The type being constructed. |
| var type = constructor.type; |
| pieces.token(type.name); |
| pieces.visit(type.typeArguments); |
| |
| // If this is a named constructor call, the name. |
| if (constructor.name case var name?) { |
| pieces.token(constructor.period); |
| pieces.visit(name); |
| } |
| |
| pieces.visit(node.argumentList); |
| } |
| |
| @override |
| void visitIntegerLiteral(IntegerLiteral node) { |
| pieces.token(node.literal); |
| } |
| |
| @override |
| void visitInterpolationExpression(InterpolationExpression node) { |
| var piece = pieces.build(() { |
| pieces.token(node.leftBracket); |
| pieces.visit(node.expression); |
| pieces.token(node.rightBracket); |
| }); |
| |
| // Don't allow splitting inside interpolated expressions (except for |
| // mandatory splits from comments and sequences). Splits inside |
| // interpolations almost never look good. It's usually better to just let |
| // the lines overflow. More importantly, a single string literal with many |
| // interpolations can easily lead to combinatorial performance in the |
| // solver. |
| // TODO(rnystrom): Traversing the entire interpolation Piece tree and |
| // pinning it feels sort of inelegant. Is there a cleaner approach? |
| void traverse(Piece piece) { |
| piece.preventSplit(); |
| piece.forEachChild(traverse); |
| } |
| |
| traverse(piece); |
| |
| if (style.is3Dot7) { |
| pieces.add(piece); |
| } else { |
| // Wrap in grouping so that an infix split inside the interpolation |
| // doesn't get collapsed in the surrounding context, like: |
| // |
| // // Wrong: |
| // var x = |
| // '${"a" |
| // "b"}'; |
| // |
| // // Right: |
| // var x = |
| // '${"a" |
| // "b"}'; |
| pieces.add(GroupingPiece(piece)); |
| } |
| } |
| |
| @override |
| void visitInterpolationString(InterpolationString node) { |
| if (_parentContext == NodeContext.multilineStringInterpolation) { |
| pieces.multilineToken(node.contents); |
| } else { |
| pieces.token(node.contents); |
| } |
| } |
| |
| @override |
| void visitIsExpression(IsExpression node) { |
| writeInfix( |
| node.expression, |
| node.isOperator, |
| operator2: node.notOperator, |
| node.type, |
| // Don't use Indent.infix after 3.7 because it will flatten the |
| // indentation if the expression occurs in an assignment. |
| // TODO(rnystrom): We probably do want to format `as` the same as other |
| // binary operators, but the tall style already treats them differently, |
| // so leaving that alone for now. |
| indent: style.is3Dot7 ? Indent.infix : Indent.expression, |
| ); |
| } |
| |
| @override |
| void visitLabel(Label node) { |
| pieces.visit(node.label); |
| pieces.token(node.colon); |
| } |
| |
| @override |
| void visitLabeledStatement(LabeledStatement node) { |
| var sequence = SequenceBuilder(this); |
| for (var label in node.labels) { |
| sequence.visit(label); |
| } |
| |
| sequence.visit(node.statement); |
| pieces.add(sequence.build()); |
| } |
| |
| @override |
| void visitLibraryDirective(LibraryDirective node) { |
| pieces.withMetadata(node.metadata, () { |
| pieces.token(node.libraryKeyword); |
| pieces.visit(node.name, spaceBefore: true); |
| pieces.token(node.semicolon); |
| }); |
| } |
| |
| @override |
| void visitLibraryIdentifier(LibraryIdentifier node) { |
| writeDotted(node.components); |
| } |
| |
| @override |
| void visitListLiteral(ListLiteral node) { |
| writeCollection( |
| constKeyword: node.constKeyword, |
| typeArguments: node.typeArguments, |
| node.leftBracket, |
| node.elements, |
| node.rightBracket, |
| splitEagerly: true, |
| preserveNewlines: true, |
| ); |
| } |
| |
| @override |
| void visitListPattern(ListPattern node) { |
| writeCollection( |
| typeArguments: node.typeArguments, |
| node.leftBracket, |
| node.elements, |
| node.rightBracket, |
| ); |
| } |
| |
| @override |
| void visitLogicalAndPattern(LogicalAndPattern node) { |
| var indent = true; |
| if (style.is3Dot7) { |
| // If a logical and pattern occurs inside a map pattern entry, we want to |
| // format the operands in parallel, like: |
| // |
| // var { |
| // key: |
| // operand1 && |
| // operand2, |
| // } = value; |
| indent = _parentContext != NodeContext.assignment; |
| } |
| |
| writeInfixChain<LogicalAndPattern>( |
| node, |
| precedence: node.operator.type.precedence, |
| indent: indent, |
| (expression) => ( |
| expression.leftOperand, |
| expression.operator, |
| expression.rightOperand, |
| ), |
| ); |
| } |
| |
| @override |
| void visitLogicalOrPattern(LogicalOrPattern node) { |
| // If a logical or pattern is the outermost pattern in a switch expression |
| // case, we flatten the operands like parallel cases: |
| // |
| // e = switch (obj) { |
| // operand1 || |
| // operand2 => value, |
| // }; |
| var indent = _parentContext != NodeContext.switchExpressionCase; |
| |
| if (style.is3Dot7) { |
| // If a logical or pattern occurs inside a map pattern entry, we want to |
| // format the operands in parallel, like: |
| // |
| // var { |
| // key: |
| // operand1 || |
| // operand2, |
| // } = value; |
| if (_parentContext == NodeContext.assignment) indent = false; |
| } |
| |
| writeInfixChain<LogicalOrPattern>( |
| node, |
| precedence: node.operator.type.precedence, |
| indent: indent, |
| (expression) => ( |
| expression.leftOperand, |
| expression.operator, |
| expression.rightOperand, |
| ), |
| ); |
| } |
| |
| @override |
| void visitMapLiteralEntry(MapLiteralEntry node) { |
| if (style.is3Dot7) { |
| writeAssignment(node.key, node.separator, node.value); |
| } else { |
| var leftPiece = pieces.build(() { |
| pieces.token(node.keyQuestion); |
| pieces.visit(node.key); |
| pieces.token(node.separator); |
| }); |
| |
| var rightPiece = pieces.build(() { |
| pieces.token(node.valueQuestion); |
| pieces.visit(node.value); |
| }); |
| |
| pieces.add(AssignPiece(leftPiece, rightPiece)); |
| } |
| } |
| |
| @override |
| void visitMapPattern(MapPattern node) { |
| writeCollection( |
| typeArguments: node.typeArguments, |
| node.leftBracket, |
| node.elements, |
| node.rightBracket, |
| ); |
| } |
| |
| @override |
| void visitMapPatternEntry(MapPatternEntry node) { |
| writeAssignment(node.key, node.separator, node.value); |
| } |
| |
| @override |
| void visitMethodDeclaration(MethodDeclaration node) { |
| writeFunction( |
| metadata: node.metadata, |
| modifiers: [node.externalKeyword, node.modifierKeyword], |
| returnType: node.returnType, |
| propertyKeyword: node.operatorKeyword ?? node.propertyKeyword, |
| name: node.name, |
| typeParameters: node.typeParameters, |
| parameters: node.parameters, |
| body: node.body, |
| ); |
| } |
| |
| @override |
| void visitMethodInvocation(MethodInvocation node) { |
| // If there's no target, this is a "bare" function call like "foo(1, 2)", |
| // or a section in a cascade. |
| // |
| // If it looks like a constructor or static call, we want to keep the |
| // target and method together instead of including the method in the |
| // subsequent method chain. |
| if (node.target == null || node.looksLikeStaticCall) { |
| pieces.visit(node.target); |
| pieces.token(node.operator); |
| pieces.visit(node.methodName); |
| pieces.visit(node.typeArguments); |
| pieces.visit(node.argumentList); |
| return; |
| } |
| |
| writeChain(node); |
| } |
| |
| @override |
| void visitMixinDeclaration(MixinDeclaration node) { |
| writeType( |
| node.metadata, |
| [node.baseKeyword, node.mixinKeyword], |
| node.name, |
| typeParameters: node.typeParameters, |
| onClause: node.onClause, |
| implementsClause: node.implementsClause, |
| body: () { |
| return pieces.build(() { |
| writeBody(node.leftBracket, node.members, node.rightBracket); |
| }); |
| }, |
| ); |
| } |
| |
| @override |
| void visitMixinOnClause(MixinOnClause node) { |
| throw UnsupportedError( |
| 'This node is handled by PieceFactory.createType().', |
| ); |
| } |
| |
| @override |
| void visitNamedExpression(NamedExpression node) { |
| writeAssignment( |
| node.name.label, |
| node.name.colon, |
| node.expression, |
| rightHandSideContext: NodeContext.namedExpression, |
| ); |
| } |
| |
| @override |
| void visitNamedType(NamedType node) { |
| pieces.token(node.importPrefix?.name); |
| pieces.token(node.importPrefix?.period); |
| pieces.token(node.name); |
| pieces.visit(node.typeArguments); |
| pieces.token(node.question); |
| } |
| |
| @override |
| void visitNativeClause(NativeClause node) { |
| pieces.token(node.nativeKeyword); |
| pieces.visit(node.name, spaceBefore: true); |
| } |
| |
| @override |
| void visitNativeFunctionBody(NativeFunctionBody node) { |
| pieces.space(); |
| pieces.token(node.nativeKeyword); |
| pieces.visit(node.stringLiteral, spaceBefore: true); |
| pieces.token(node.semicolon); |
| } |
| |
| @override |
| void visitNullAssertPattern(NullAssertPattern node) { |
| writePostfix(node.pattern, node.operator); |
| } |
| |
| @override |
| void visitNullAwareElement(NullAwareElement node) { |
| writePrefix(node.question, node.value); |
| } |
| |
| @override |
| void visitNullCheckPattern(NullCheckPattern node) { |
| writePostfix(node.pattern, node.operator); |
| } |
| |
| @override |
| void visitNullLiteral(NullLiteral node) { |
| pieces.token(node.literal); |
| } |
| |
| @override |
| void visitObjectPattern(ObjectPattern node) { |
| // If the object pattern is completely empty, write it inline so that we |
| // create fewer pieces. |
| if (!node.fields.canSplit(node.rightParenthesis)) { |
| pieces.visit(node.type); |
| pieces.token(node.leftParenthesis); |
| pieces.token(node.rightParenthesis); |
| return; |
| } |
| |
| var builder = DelimitedListBuilder(this); |
| |
| builder.addLeftBracket( |
| pieces.build(() { |
| pieces.visit(node.type); |
| pieces.token(node.leftParenthesis); |
| }), |
| ); |
| |
| node.fields.forEach(builder.visit); |
| builder.rightBracket(node.rightParenthesis); |
| pieces.add( |
| builder.build( |
| forceSplit: style.preserveTrailingCommaBefore(node.rightParenthesis), |
| ), |
| ); |
| } |
| |
| @override |
| void visitParenthesizedExpression(ParenthesizedExpression node) { |
| writeParenthesized( |
| node.leftParenthesis, |
| node.expression, |
| node.rightParenthesis, |
| ); |
| } |
| |
| @override |
| void visitParenthesizedPattern(ParenthesizedPattern node) { |
| writeParenthesized( |
| node.leftParenthesis, |
| node.pattern, |
| node.rightParenthesis, |
| ); |
| } |
| |
| @override |
| void visitPartDirective(PartDirective node) { |
| pieces.withMetadata(node.metadata, () { |
| pieces.token(node.partKeyword); |
| pieces.space(); |
| pieces.visit(node.uri); |
| pieces.token(node.semicolon); |
| }); |
| } |
| |
| @override |
| void visitPartOfDirective(PartOfDirective node) { |
| pieces.withMetadata(node.metadata, () { |
| pieces.token(node.partKeyword); |
| pieces.space(); |
| pieces.token(node.ofKeyword); |
| pieces.space(); |
| |
| // Part-of may have either a name or a URI. Only one of these will be |
| // non-null. We visit both since visit() ignores null. |
| pieces.visit(node.libraryName); |
| pieces.visit(node.uri); |
| pieces.token(node.semicolon); |
| }); |
| } |
| |
| @override |
| void visitPatternAssignment(PatternAssignment node) { |
| writeAssignment(node.pattern, node.equals, node.expression); |
| } |
| |
| @override |
| void visitPatternField(PatternField node) { |
| pieces.visit(node.name); |
| pieces.visit(node.pattern); |
| } |
| |
| @override |
| void visitPatternFieldName(PatternFieldName node) { |
| pieces.token(node.name); |
| pieces.token(node.colon); |
| if (node.name != null) pieces.space(); |
| } |
| |
| @override |
| void visitPatternVariableDeclaration(PatternVariableDeclaration node) { |
| pieces.withMetadata( |
| node.metadata, |
| // If the variable is part of a for loop, it looks weird to force the |
| // metadata to split since it's in a sort of expression-ish location: |
| // |
| // for (@meta var (x, y) in pairs) ... |
| inlineMetadata: _parentContext == NodeContext.forLoopVariable, |
| () { |
| pieces.token(node.keyword); |
| pieces.space(); |
| writeAssignment(node.pattern, node.equals, node.expression); |
| }, |
| ); |
| } |
| |
| @override |
| void visitPatternVariableDeclarationStatement( |
| PatternVariableDeclarationStatement node, |
| ) { |
| pieces.visit(node.declaration); |
| pieces.token(node.semicolon); |
| } |
| |
| @override |
| void visitPostfixExpression(PostfixExpression node) { |
| writePostfix(node.operand, node.operator); |
| } |
| |
| @override |
| void visitPrefixedIdentifier(PrefixedIdentifier node) { |
| writeChain(node); |
| } |
| |
| @override |
| void visitPrefixExpression(PrefixExpression node) { |
| // Edge case: If we have two adjacent `-` or `--` operators, insert a space |
| // between them so that we don't inadvertently collapse `- -` into `--`. |
| var space = switch ((node.operator, node.operand)) { |
| ( |
| Token(lexeme: '-' || '--'), |
| PrefixExpression(operator: Token(lexeme: '-' || '--')), |
| ) => |
| true, |
| _ => false, |
| }; |
| |
| writePrefix(node.operator, node.operand, space: space); |
| } |
| |
| @override |
| void visitPropertyAccess(PropertyAccess node) { |
| // If there's no target, this is a section in a cascade. |
| if (node.target == null) { |
| pieces.token(node.operator); |
| pieces.visit(node.propertyName); |
| return; |
| } |
| |
| writeChain(node); |
| } |
| |
| @override |
| void visitRedirectingConstructorInvocation( |
| RedirectingConstructorInvocation node, |
| ) { |
| pieces.token(node.thisKeyword); |
| pieces.token(node.period); |
| pieces.visit(node.constructorName); |
| pieces.visit(node.argumentList); |
| } |
| |
| @override |
| void visitRecordLiteral(RecordLiteral node) { |
| writeRecord( |
| constKeyword: node.constKeyword, |
| node.leftParenthesis, |
| node.fields, |
| node.rightParenthesis, |
| preserveNewlines: true, |
| ); |
| } |
| |
| @override |
| void visitRecordPattern(RecordPattern node) { |
| writeRecord(node.leftParenthesis, node.fields, node.rightParenthesis); |
| } |
| |
| @override |
| void visitRecordTypeAnnotation(RecordTypeAnnotation node) { |
| var namedFields = node.namedFields; |
| var positionalFields = node.positionalFields; |
| |
| // Single positional record types always have a trailing comma and are not |
| // forced to split. |
| var isSinglePositional = |
| positionalFields.length == 1 && namedFields == null; |
| |
| var listStyle = isSinglePositional |
| ? const ListStyle(commas: Commas.alwaysTrailing) |
| : const ListStyle(commas: Commas.trailing); |
| var builder = DelimitedListBuilder(this, listStyle); |
| |
| // If all parameters are optional, put the `{` right after `(`. |
| builder.addLeftBracket( |
| pieces.build(() { |
| pieces.token(node.leftParenthesis); |
| if (positionalFields.isEmpty && namedFields != null) { |
| pieces.token(namedFields.leftBracket); |
| } |
| }), |
| ); |
| |
| for (var positionalField in positionalFields) { |
| builder.visit(positionalField); |
| } |
| |
| Token? rightDelimiter; |
| if (namedFields != null) { |
| // If we have both positional fields and named fields, then we need to add |
| // the left bracket delimiter before the first named field. |
| if (positionalFields.isNotEmpty) { |
| builder.leftDelimiter(namedFields.leftBracket); |
| } |
| for (var namedField in namedFields.fields) { |
| builder.visit(namedField); |
| } |
| rightDelimiter = namedFields.rightBracket; |
| } |
| |
| builder.rightBracket(node.rightParenthesis, delimiter: rightDelimiter); |
| pieces.add( |
| builder.build( |
| forceSplit: |
| !isSinglePositional && |
| style.preserveTrailingCommaBefore( |
| rightDelimiter ?? node.rightParenthesis, |
| ), |
| ), |
| ); |
| pieces.token(node.question); |
| } |
| |
| @override |
| void visitRecordTypeAnnotationNamedField( |
| RecordTypeAnnotationNamedField node, |
| ) { |
| writeRecordTypeField(node); |
| } |
| |
| @override |
| void visitRecordTypeAnnotationPositionalField( |
| RecordTypeAnnotationPositionalField node, |
| ) { |
| writeRecordTypeField(node); |
| } |
| |
| @override |
| void visitRelationalPattern(RelationalPattern node) { |
| pieces.token(node.operator); |
| pieces.space(); |
| pieces.visit(node.operand); |
| } |
| |
| @override |
| void visitRepresentationConstructorName(RepresentationConstructorName node) { |
| pieces.token(node.period); |
| pieces.token(node.name); |
| } |
| |
| @override |
| void visitRepresentationDeclaration(RepresentationDeclaration node) { |
| pieces.visit(node.constructorName); |
| |
| var builder = DelimitedListBuilder( |
| this, |
| const ListStyle(commas: Commas.nonTrailing), |
| ); |
| builder.leftBracket(node.leftParenthesis); |
| builder.add( |
| pieces.build(() { |
| writeParameter( |
| metadata: node.fieldMetadata, |
| node.fieldType, |
| node.fieldName, |
| ); |
| }), |
| ); |
| builder.rightBracket(node.rightParenthesis); |
| pieces.add(builder.build()); |
| } |
| |
| @override |
| void visitRethrowExpression(RethrowExpression node) { |
| pieces.token(node.rethrowKeyword); |
| } |
| |
| @override |
| void visitRestPatternElement(RestPatternElement node) { |
| writePrefix(node.operator, node.pattern); |
| } |
| |
| @override |
| void visitReturnStatement(ReturnStatement node) { |
| pieces.token(node.returnKeyword); |
| pieces.visit(node.expression, spaceBefore: true); |
| pieces.token(node.semicolon); |
| } |
| |
| @override |
| void visitScriptTag(ScriptTag node) { |
| pieces.token(node.scriptTag); |
| } |
| |
| @override |
| void visitSetOrMapLiteral(SetOrMapLiteral node) { |
| writeCollection( |
| constKeyword: node.constKeyword, |
| typeArguments: node.typeArguments, |
| node.leftBracket, |
| node.elements, |
| node.rightBracket, |
| splitEagerly: true, |
| preserveNewlines: true, |
| ); |
| } |
| |
| @override |
| void visitShowCombinator(ShowCombinator node) { |
| throw UnsupportedError('Combinators are handled by createImport().'); |
| } |
| |
| @override |
| void visitSimpleFormalParameter(SimpleFormalParameter node) { |
| writeFormalParameter( |
| node, |
| node.type, |
| node.name, |
| mutableKeyword: node.keyword, |
| ); |
| } |
| |
| @override |
| void visitSimpleIdentifier(SimpleIdentifier node) { |
| pieces.token(node.token); |
| } |
| |
| @override |
| void visitSimpleStringLiteral(SimpleStringLiteral node) { |
| if (node.isMultiline) { |
| pieces.multilineToken(node.literal); |
| } else { |
| pieces.token(node.literal); |
| } |
| } |
| |
| @override |
| void visitSpreadElement(SpreadElement node) { |
| writePrefix(node.spreadOperator, node.expression); |
| } |
| |
| @override |
| void visitStringInterpolation(StringInterpolation node) { |
| for (var element in node.elements) { |
| pieces.visit( |
| element, |
| context: node.isMultiline |
| ? NodeContext.multilineStringInterpolation |
| : NodeContext.none, |
| ); |
| } |
| } |
| |
| @override |
| void visitSuperConstructorInvocation(SuperConstructorInvocation node) { |
| pieces.token(node.superKeyword); |
| pieces.token(node.period); |
| pieces.visit(node.constructorName); |
| pieces.visit(node.argumentList); |
| } |
| |
| @override |
| void visitSuperExpression(SuperExpression node) { |
| pieces.token(node.superKeyword); |
| } |
| |
| @override |
| void visitSuperFormalParameter(SuperFormalParameter node) { |
| if (node.parameters case var parameters?) { |
| // A function-typed super parameter like: |
| // |
| // C(super.fn(parameter)); |
| writeFunctionType( |
| node.type, |
| fieldKeyword: node.superKeyword, |
| period: node.period, |
| node.name, |
| node.typeParameters, |
| parameters, |
| node.question, |
| parameter: node, |
| ); |
| } else { |
| writeFormalParameter( |
| node, |
| mutableKeyword: node.keyword, |
| fieldKeyword: node.superKeyword, |
| period: node.period, |
| node.type, |
| node.name, |
| ); |
| } |
| } |
| |
| @override |
| void visitSwitchExpression(SwitchExpression node) { |
| var list = DelimitedListBuilder( |
| this, |
| const ListStyle(spaceWhenUnsplit: true), |
| ); |
| |
| list.addLeftBracket( |
| pieces.build(() { |
| writeControlFlowStart( |
| node.switchKeyword, |
| node.leftParenthesis, |
| node.expression, |
| node.rightParenthesis, |
| ); |
| pieces.space(); |
| pieces.token(node.leftBracket); |
| }), |
| ); |
| |
| for (var member in node.cases) { |
| list.visit(member); |
| } |
| |
| list.rightBracket(node.rightBracket); |
| pieces.add(list.build(forceSplit: node.cases.isNotEmpty)); |
| } |
| |
| @override |
| void visitSwitchExpressionCase(SwitchExpressionCase node) { |
| var patternPiece = nodePiece( |
| node.guardedPattern.pattern, |
| context: NodeContext.switchExpressionCase, |
| ); |
| |
| var guardPiece = optionalNodePiece(node.guardedPattern.whenClause); |
| var arrowPiece = tokenPiece(node.arrow); |
| var bodyPiece = nodePiece(node.expression); |
| |
| pieces.add( |
| CaseExpressionPiece( |
| patternPiece, |
| guardPiece, |
| arrowPiece, |
| bodyPiece, |
| canBlockSplitPattern: node.guardedPattern.pattern.canBlockSplit, |
| patternIsLogicalOr: node.guardedPattern.pattern is LogicalOrPattern, |
| canBlockSplitBody: !style.is3Dot7 || node.expression.canBlockSplit, |
| ), |
| ); |
| } |
| |
| @override |
| void visitSwitchStatement(SwitchStatement node) { |
| writeControlFlowStart( |
| node.switchKeyword, |
| node.leftParenthesis, |
| node.expression, |
| node.rightParenthesis, |
| ); |
| pieces.space(); |
| |
| var sequence = SequenceBuilder(this); |
| sequence.leftBracket(node.leftBracket); |
| |
| for (var member in node.members) { |
| for (var label in member.labels) { |
| sequence.visit(label); |
| } |
| |
| sequence.addCommentsBefore(member.keyword); |
| |
| var casePiece = pieces.build(() { |
| pieces.token(member.keyword); |
| |
| switch (member) { |
| case SwitchCase(): |
| pieces.space(); |
| pieces.visit(member.expression); |
| case SwitchPatternCase(): |
| pieces.space(); |
| |
| var patternPiece = nodePiece(member.guardedPattern.pattern); |
| |
| if (member.guardedPattern.whenClause case var whenClause?) { |
| pieces.add( |
| InfixPiece([ |
| patternPiece, |
| nodePiece(whenClause), |
| ], is3Dot7: style.is3Dot7), |
| ); |
| } else { |
| pieces.add(patternPiece); |
| } |
| |
| case SwitchDefault(): |
| break; // Nothing to do. |
| } |
| |
| pieces.token(member.colon); |
| }); |
| |
| // Don't allow any blank lines between the `case` line and the first |
| // statement in the case (or the next case if this case has no body). |
| sequence.add(casePiece, indent: Indent.none, allowBlankAfter: false); |
| |
| for (var statement in member.statements) { |
| sequence.visit(statement, indent: Indent.block); |
| } |
| } |
| |
| sequence.rightBracket(node.rightBracket); |
| pieces.add(sequence.build()); |
| } |
| |
| @override |
| void visitSymbolLiteral(SymbolLiteral node) { |
| pieces.token(node.poundSign); |
| var components = node.components; |
| for (var component in components) { |
| // The '.' separator. |
| if (component != components.first) { |
| pieces.token(component.previous!); |
| } |
| |
| pieces.token(component); |
| } |
| } |
| |
| @override |
| void visitThisExpression(ThisExpression node) { |
| pieces.token(node.thisKeyword); |
| } |
| |
| @override |
| void visitThrowExpression(ThrowExpression node) { |
| writePrefix(node.throwKeyword, space: true, node.expression); |
| } |
| |
| @override |
| void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { |
| pieces.withMetadata(node.metadata, () { |
| pieces.modifier(node.externalKeyword); |
| pieces.visit(node.variables); |
| pieces.token(node.semicolon); |
| }); |
| } |
| |
| @override |
| void visitTryStatement(TryStatement node) { |
| writeTry(node); |
| } |
| |
| @override |
| void visitTypeArgumentList(TypeArgumentList node) { |
| writeTypeList(node.leftBracket, node.arguments, node.rightBracket); |
| } |
| |
| @override |
| void visitTypeParameter(TypeParameter node) { |
| pieces.withMetadata(node.metadata, inlineMetadata: true, () { |
| if (node.bound case var bound?) { |
| var typeParameterPiece = pieces.build(() { |
| pieces.token(node.name); |
| }); |
| |
| var boundPiece = pieces.build(() { |
| pieces.token(node.extendsKeyword); |
| pieces.space(); |
| pieces.visit(bound); |
| }); |
| |
| pieces.add(TypeParameterBoundPiece(typeParameterPiece, boundPiece)); |
| } else { |
| // No bound. |
| pieces.token(node.name); |
| } |
| }); |
| } |
| |
| @override |
| void visitTypeParameterList(TypeParameterList node) { |
| writeTypeList(node.leftBracket, node.typeParameters, node.rightBracket); |
| } |
| |
| @override |
| void visitVariableDeclaration(VariableDeclaration node) { |
| throw UnsupportedError('This is handled by visitVariableDeclarationList()'); |
| } |
| |
| @override |
| void visitVariableDeclarationList(VariableDeclarationList node) { |
| pieces.withMetadata( |
| node.metadata, |
| // If the variable is part of a for loop, it looks weird to force the |
| // metadata to split since it's in a sort of expression-ish location: |
| // |
| // for (@meta var x in list) ... |
| inlineMetadata: _parentContext == NodeContext.forLoopVariable, |
| () { |
| var header = pieces.build(() { |
| pieces.modifier(node.lateKeyword); |
| pieces.modifier(node.keyword); |
| pieces.visit(node.type); |
| }); |
| |
| var variables = <Piece>[]; |
| |
| for (var variable in node.variables) { |
| if ((variable.equals, variable.initializer) case ( |
| var equals?, |
| var initializer?, |
| )) { |
| if (style.is3Dot7) { |
| var variablePiece = tokenPiece(variable.name); |
| |
| var equalsPiece = pieces.build(() { |
| pieces.space(); |
| pieces.token(equals); |
| }); |
| |
| var initializerPiece = nodePiece( |
| initializer, |
| commaAfter: true, |
| context: NodeContext.assignment, |
| ); |
| |
| variables.add( |
| AssignPiece3Dot7( |
| left: variablePiece, |
| equalsPiece, |
| initializerPiece, |
| canBlockSplitRight: initializer.canBlockSplit, |
| ), |
| ); |
| } else { |
| var variablePiece = pieces.build(() { |
| pieces.token(variable.name); |
| pieces.space(); |
| pieces.token(equals); |
| }); |
| |
| var initializerPiece = nodePiece(initializer, commaAfter: true); |
| |
| variables.add(AssignPiece(variablePiece, initializerPiece)); |
| } |
| } else { |
| variables.add(tokenPiece(variable.name, commaAfter: true)); |
| } |
| } |
| |
| pieces.add( |
| VariablePiece( |
| header, |
| variables, |
| hasType: node.type != null, |
| is3Dot7: style.is3Dot7, |
| ), |
| ); |
| }, |
| ); |
| } |
| |
| @override |
| void visitVariableDeclarationStatement(VariableDeclarationStatement node) { |
| pieces.visit(node.variables); |
| pieces.token(node.semicolon); |
| } |
| |
| @override |
| void visitWhenClause(WhenClause node) { |
| writePrefix(node.whenKeyword, space: true, node.expression); |
| } |
| |
| @override |
| void visitWhileStatement(WhileStatement node) { |
| var condition = pieces.build(() { |
| writeControlFlowStart( |
| node.whileKeyword, |
| node.leftParenthesis, |
| node.condition, |
| node.rightParenthesis, |
| ); |
| pieces.space(); |
| }); |
| |
| var body = nodePiece(node.body); |
| |
| var piece = ControlFlowPiece(); |
| piece.add(condition, body, isBlock: node.body is Block); |
| pieces.add(piece); |
| } |
| |
| @override |
| void visitWildcardPattern(WildcardPattern node) { |
| writePatternVariable(node.keyword, node.type, node.name); |
| } |
| |
| @override |
| void visitWithClause(WithClause node) { |
| throw UnsupportedError( |
| 'This node is handled by PieceFactory.createType().', |
| ); |
| } |
| |
| @override |
| void visitYieldStatement(YieldStatement node) { |
| pieces.token(node.yieldKeyword); |
| pieces.token(node.star); |
| pieces.space(); |
| pieces.visit(node.expression); |
| pieces.token(node.semicolon); |
| } |
| |
| /// Visits [node] in [context]. |
| @override |
| void visitNode(AstNode node, NodeContext context) { |
| var previousContext = _parentContext; |
| _parentContext = context; |
| |
| // If there are comments before this node, then some of them may be leading |
| // comments. If so, capture them now. We do this here so that the comments |
| // are wrapped around the outermost possible Piece for the AST node. For |
| // example: |
| // |
| // // Comment |
| // a.b && c || d ? e : f; |
| // |
| // Here, the node that actually owns the token before the comment is `a`, |
| // which is an identifier expression inside a property access inside an |
| // `&&` expression inside an `||` expression inside `?:` expression. If we |
| // attach the comment to the identifier expression, then the newline from |
| // the comment will force all of those surrounding pieces to split: |
| // |
| // // Comment |
| // a |
| // .b && |
| // c || |
| // d |
| // ? e |
| // : f; |
| // |
| // Instead, we hoist the comment out of all of those and then have comment |
| // precede them all so that they don't split. |
| var firstToken = node.firstNonCommentToken; |
| if (firstToken.precedingComments != null) { |
| var comments = pieces.takeCommentsBefore(firstToken); |
| var piece = pieces.build(() { |
| node.accept(this); |
| }); |
| |
| // Check again because the preceding comments may not necessarily end up |
| // as leading comments. |
| if (comments.isNotEmpty) piece = LeadingCommentPiece(comments, piece); |
| |
| pieces.add(piece); |
| } else { |
| // No preceding comments, so just visit it inline. |
| node.accept(this); |
| } |
| |
| _parentContext = previousContext; |
| } |
| } |