| // Copyright (c) 2014, 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. |
| // ignore_for_file: avoid_dynamic_calls |
| |
| 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'; |
| // ignore: implementation_imports |
| import 'package:analyzer/src/clients/dart_style/rewrite_cascade.dart'; |
| |
| import 'ast_extensions.dart'; |
| import 'call_chain_visitor.dart'; |
| import 'chunk.dart'; |
| import 'chunk_builder.dart'; |
| import 'constants.dart'; |
| import 'dart_formatter.dart'; |
| import 'rule/argument.dart'; |
| import 'rule/combinator.dart'; |
| import 'rule/conditional.dart'; |
| import 'rule/initializer.dart'; |
| import 'rule/rule.dart'; |
| import 'rule/type_argument.dart'; |
| import 'source_code.dart'; |
| import 'style_fix.dart'; |
| |
| // TODO: Handle comments around trailing commas when the comma is added or |
| // removed. |
| |
| // TODO: Write tests for how the selection gets updated when it is over or near |
| // an added or removed trailing comma. |
| |
| /// Visits every token of the AST and passes all of the relevant bits to a |
| /// [ChunkBuilder]. |
| class SourceVisitor extends ThrowingAstVisitor { |
| /// The builder for the block that is currently being visited. |
| ChunkBuilder builder; |
| |
| final DartFormatter _formatter; |
| |
| /// Cached line info for calculating blank lines. |
| final LineInfo _lineInfo; |
| |
| /// The source being formatted. |
| final SourceCode _source; |
| |
| /// The most recently written token. |
| /// |
| /// This is used to determine how many lines are between a pair of tokens in |
| /// the original source in places where a user can control whether or not a |
| /// blank line or newline is left in the output. |
| late Token _lastToken; |
| |
| /// `true` if the visitor has written past the beginning of the selection in |
| /// the original source text. |
| bool _passedSelectionStart = false; |
| |
| /// `true` if the visitor has written past the end of the selection in the |
| /// original source text. |
| bool _passedSelectionEnd = false; |
| |
| /// The character offset of the end of the selection, if there is a selection. |
| /// |
| /// This is calculated and cached by [_findSelectionEnd]. |
| int? _selectionEnd; |
| |
| /// How many levels deep inside a constant context the visitor currently is. |
| int _constNesting = 0; |
| |
| /// Whether we are currently fixing a typedef declaration. |
| /// |
| /// Set to `true` while traversing the parameters of a typedef being converted |
| /// to the new syntax. The new syntax does not allow `int foo()` as a |
| /// parameter declaration, so it needs to be converted to `int Function() foo` |
| /// as part of the fix. |
| bool _insideNewTypedefFix = false; |
| |
| /// A stack that tracks forcing nested collections to split. |
| /// |
| /// Each entry corresponds to a collection currently being visited and the |
| /// value is whether or not it should be forced to split. Every time a |
| /// collection is entered, it sets all of the existing elements to `true` |
| /// then it pushes `false` for itself. |
| /// |
| /// When done visiting the elements, it removes its value. If it was set to |
| /// `true`, we know we visited a nested collection so we force this one to |
| /// split. |
| final List<bool> _collectionSplits = []; |
| |
| // TODO: This is only used to ensure that the then and else spread collections |
| // split together. Can we do that in a simpler way? |
| /// Associates delimited block expressions with the rule for the containing |
| /// expression that manages them. |
| /// |
| /// This is used for spread collection literals inside control flow elements. |
| final Map<Token, Rule> _blockCollectionRules = {}; |
| |
| /// Comments and new lines attached to tokens added here are suppressed |
| /// from the output. |
| final Set<Token> _suppressPrecedingCommentsAndNewLines = {}; |
| |
| /// Initialize a newly created visitor to write source code representing |
| /// the visited nodes to the given [writer]. |
| SourceVisitor(this._formatter, this._lineInfo, this._source) |
| : builder = ChunkBuilder(_formatter, _source); |
| |
| /// Runs the visitor on [node], formatting its contents. |
| /// |
| /// 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(AstNode node) { |
| visit(node); |
| |
| // Output trailing comments. |
| writePrecedingCommentsAndNewlines(node.endToken.next!); |
| |
| assert(_constNesting == 0, 'Should have exited all const contexts.'); |
| |
| // Finish writing and return the complete result. |
| return builder.end(); |
| } |
| |
| @override |
| void visitAdjacentStrings(AdjacentStrings node) { |
| // We generally want to indent adjacent strings because it can be confusing |
| // otherwise when they appear in a list of expressions, like: |
| // |
| // [ |
| // "one", |
| // "two" |
| // "three", |
| // "four" |
| // ] |
| // |
| // Especially when these stings are longer, it can be hard to tell that |
| // "three" is a continuation of the previous argument. |
| // |
| // However, the indentation is distracting in argument lists that don't |
| // suffer from this ambiguity: |
| // |
| // test( |
| // "A very long test description..." |
| // "this indentation looks bad.", () { ... }); |
| // |
| // To balance these, we omit the indentation when an adjacent string |
| // expression is the only string in an argument list. |
| var shouldNest = true; |
| |
| var parent = node.parent; |
| if (parent is ArgumentList) { |
| shouldNest = false; |
| |
| for (var argument in parent.arguments) { |
| if (argument == node) continue; |
| if (argument is StringLiteral) { |
| shouldNest = true; |
| break; |
| } |
| } |
| } else if (parent is Assertion) { |
| // Treat asserts like argument lists. |
| shouldNest = false; |
| if (parent.condition != node && parent.condition is StringLiteral) { |
| shouldNest = true; |
| } |
| |
| if (parent.message != node && parent.message is StringLiteral) { |
| shouldNest = true; |
| } |
| } else if (parent is VariableDeclaration || |
| parent is AssignmentExpression && |
| parent.rightHandSide == node && |
| parent.parent is ExpressionStatement) { |
| // Don't add extra indentation in a variable initializer or assignment: |
| // |
| // var variable = |
| // "no extra" |
| // "indent"; |
| shouldNest = false; |
| } else if (parent is NamedExpression || parent is ExpressionFunctionBody) { |
| shouldNest = false; |
| } |
| |
| builder.startSpan(); |
| builder.startRule(Rule(Cost.adjacentStrings)); |
| if (shouldNest) { |
| // TODO: Hack. Figure out better way to handle conditional expressions. |
| builder.nestExpression( |
| indent: node.parent is ConditionalExpression |
| ? Indent.block |
| : Indent.expression); |
| } |
| visitNodes(node.strings, between: splitOrNewline); |
| if (shouldNest) builder.unnest(); |
| builder.endRule(); |
| builder.endSpan(); |
| } |
| |
| @override |
| void visitAnnotation(Annotation node) { |
| token(node.atSign); |
| visit(node.name); |
| |
| builder.nestExpression(); |
| visit(node.typeArguments); |
| token(node.period); |
| visit(node.constructorName); |
| |
| if (node.arguments != null) { |
| // Metadata annotations are always const contexts. |
| _constNesting++; |
| visitArgumentList(node.arguments!); |
| _constNesting--; |
| } |
| |
| builder.unnest(); |
| } |
| |
| // TODO: Update doc. |
| /// Visits an argument list. |
| /// |
| /// This is a bit complex to handle the rules for formatting positional and |
| /// named arguments. The goals, in rough order of descending priority are: |
| /// |
| /// 1. Keep everything on the first line. |
| /// 2. Keep the named arguments together on the next line. |
| /// 3. Keep everything together on the second line. |
| /// 4. Split between one or more positional arguments, trying to keep as many |
| /// on earlier lines as possible. |
| /// 5. Split the named arguments each onto their own line. |
| @override |
| Rule? visitArgumentList(ArgumentList node) { |
| // Handle empty collections, with or without comments. |
| if (node.arguments.isEmpty) { |
| _visitBody(node.leftParenthesis, node.arguments, node.rightParenthesis); |
| |
| // TODO: If the empty arg list has a comment, then we should return a |
| // Rule for that. |
| return null; |
| } |
| |
| return _visitArgumentList( |
| node.leftParenthesis, node.arguments, node.rightParenthesis); |
| } |
| |
| @override |
| void visitAsExpression(AsExpression node) { |
| builder.startSpan(); |
| builder.nestExpression(); |
| visit(node.expression); |
| soloSplit(); |
| token(node.asOperator); |
| space(); |
| visit(node.type); |
| builder.unnest(); |
| builder.endSpan(); |
| } |
| |
| @override |
| void visitAssertInitializer(AssertInitializer node) { |
| _visitAssertion(node); |
| |
| // Force the initializer list to split if there are any asserts in it. |
| // Since they are statement-like, it looks weird to keep them inline. |
| builder.forceRules(); |
| } |
| |
| @override |
| void visitAssertStatement(AssertStatement node) { |
| _simpleStatement(node, () { |
| _visitAssertion(node); |
| }); |
| } |
| |
| void _visitAssertion(Assertion node) { |
| token(node.assertKeyword); |
| |
| var arguments = [node.condition, if (node.message != null) node.message!]; |
| |
| _visitArgumentList(node.leftParenthesis, arguments, node.rightParenthesis); |
| } |
| |
| @override |
| void visitAssignedVariablePattern(AssignedVariablePattern node) { |
| token(node.name); |
| } |
| |
| @override |
| void visitAssignmentExpression(AssignmentExpression node) { |
| builder.nestExpression(); |
| |
| visit(node.leftHandSide); |
| _visitAssignment(node.operator, node.rightHandSide); |
| |
| builder.unnest(); |
| } |
| |
| @override |
| void visitAwaitExpression(AwaitExpression node) { |
| token(node.awaitKeyword); |
| space(); |
| visit(node.expression); |
| } |
| |
| @override |
| void visitBinaryExpression(BinaryExpression node) { |
| // If a binary operator sequence appears immediately after a `=>`, don't |
| // add an extra level of nesting. Instead, let the subsequent operands line |
| // up with the first, as in: |
| // |
| // method() => |
| // argument && |
| // argument && |
| // argument; |
| var nest = node.parent is! ExpressionFunctionBody; |
| |
| _visitBinary<BinaryExpression>( |
| node, |
| precedence: node.operator.type.precedence, |
| nest: nest, |
| (expression) => BinaryNode(expression.leftOperand, expression.operator, |
| expression.rightOperand)); |
| } |
| |
| @override |
| void visitBlock(Block node) { |
| // Treat empty blocks specially. In most cases, they are not allowed to |
| // split. However, an empty block as the then statement of an if with an |
| // else is always split. |
| if (node.statements.isEmptyBody(node.rightBracket)) { |
| token(node.leftBracket); |
| if (_splitEmptyBlock(node)) newline(); |
| token(node.rightBracket); |
| return; |
| } |
| |
| // If this block is for a function expression in an argument list that |
| // shouldn't split the argument list, then don't. |
| _visitBody(node.leftBracket, node.statements, node.rightBracket); |
| } |
| |
| @override |
| void visitBlockFunctionBody(BlockFunctionBody node) { |
| // Space after the parameter list. |
| space(); |
| |
| // The "async" or "sync" keyword. |
| token(node.keyword); |
| |
| // The "*" in "async*" or "sync*". |
| token(node.star); |
| if (node.keyword != null) space(); |
| |
| visit(node.block); |
| } |
| |
| @override |
| void visitBooleanLiteral(BooleanLiteral node) { |
| token(node.literal); |
| } |
| |
| @override |
| void visitBreakStatement(BreakStatement node) { |
| _simpleStatement(node, () { |
| token(node.breakKeyword); |
| visit(node.label, before: space); |
| }); |
| } |
| |
| @override |
| void visitCascadeExpression(CascadeExpression node) { |
| // Optimized path if we know the cascade will split. |
| if (node.cascadeSections.length > 1) { |
| _visitSplitCascade(node); |
| return; |
| } |
| |
| // Whether a split in the cascade target expression forces the cascade to |
| // move to the next line. It looks weird to move the cascade down if the |
| // target expression is a collection, so we don't: |
| // |
| // var list = [ |
| // stuff |
| // ] |
| // ..add(more); |
| var target = node.target; |
| var splitIfTargetSplits = |
| node.cascadeSections.length > 1 || !target.isDelimitedOrCall; |
| |
| if (splitIfTargetSplits) { |
| builder.startLazyRule(node.allowInline ? Rule() : Rule.hard()); |
| } |
| |
| visit(node.target); |
| |
| builder.nestExpression(indent: Indent.cascade, now: true); |
| builder.startBlockArgumentNesting(); |
| |
| // If the cascade section shouldn't cause the cascade to split, end the |
| // rule early so it isn't affected by it. |
| if (!splitIfTargetSplits) { |
| builder.startRule(node.allowInline ? Rule() : Rule.hard()); |
| } |
| |
| zeroSplit(); |
| |
| if (!splitIfTargetSplits) builder.endRule(); |
| |
| visitNodes(node.cascadeSections, between: zeroSplit); |
| |
| if (splitIfTargetSplits) builder.endRule(); |
| |
| builder.endBlockArgumentNesting(); |
| builder.unnest(); |
| } |
| |
| /// Format the cascade using a nested block instead of a single inline |
| /// expression. |
| /// |
| /// If the cascade has multiple sections, we know each section will be on its |
| /// own line and we know there will be at least one trailing section following |
| /// a preceding one. That let's us treat all of the earlier sections as a |
| /// separate block like we do with collections and functions, instead of a |
| /// monolithic expression. Using a block in turn makes big cascades much |
| /// faster to format (like 10x) since the block formatting is memoized and |
| /// each cascade section in it is formatted independently. |
| /// |
| /// The tricky part is that block formatting assumes the entire line will be |
| /// part of the block. This is not true of the last section in a cascade, |
| /// which may have other trailing code, like the `;` here: |
| /// |
| /// var x = someLeadingExpression |
| /// ..firstCascade() |
| /// ..secondCascade() |
| /// ..thirdCascade() |
| /// ..fourthCascade(); |
| /// |
| /// To handle that, we don't put the last section in the block and instead |
| /// format it with the surrounding expression. So, from the formatter's |
| /// view, the above casade is formatted like: |
| /// |
| /// var x = someLeadingExpression |
| /// [ begin block ] |
| /// ..firstCascade() |
| /// ..secondCascade() |
| /// ..thirdCascade() |
| /// [ end block ] |
| /// ..fourthCascade(); |
| /// |
| /// This somewhere between clever and hacky, but it works and allows cascades |
| /// of essentially unbounded length to be formatted quickly. |
| void _visitSplitCascade(CascadeExpression node) { |
| // Rule to split the block. |
| builder.startLazyRule(Rule.hard()); |
| visit(node.target); |
| |
| builder.nestExpression(indent: Indent.cascade, now: true); |
| builder.startBlockArgumentNesting(); |
| |
| // If there are comments before the first section, keep them outside of the |
| // block. That way code like: |
| // |
| // receiver // comment |
| // ..cascade(); |
| // |
| // Keeps the comment on the first line. |
| var firstCommentToken = node.cascadeSections.first.beginToken; |
| writePrecedingCommentsAndNewlines(firstCommentToken); |
| _suppressPrecedingCommentsAndNewLines.add(firstCommentToken); |
| |
| // Process the inner cascade sections as a separate block. This way the |
| // entire cascade expression isn't line split as a single monolithic unit, |
| // which is very slow. |
| builder = builder.startBlock(indent: false); |
| |
| for (var i = 0; i < node.cascadeSections.length - 1; i++) { |
| newline(); |
| visit(node.cascadeSections[i]); |
| } |
| |
| // Put comments before the last section inside the block. |
| var lastCommentToken = node.cascadeSections.last.beginToken; |
| writePrecedingCommentsAndNewlines(lastCommentToken); |
| _suppressPrecedingCommentsAndNewLines.add(lastCommentToken); |
| |
| builder = builder.endBlock(); |
| |
| // The last section is outside of the block. |
| visit(node.cascadeSections.last); |
| |
| builder.endRule(); |
| builder.endBlockArgumentNesting(); |
| builder.unnest(); |
| } |
| |
| @override |
| void visitCastPattern(CastPattern node) { |
| builder.startSpan(); |
| builder.nestExpression(); |
| visit(node.pattern); |
| soloSplit(); |
| token(node.asToken); |
| space(); |
| visit(node.type); |
| builder.unnest(); |
| builder.endSpan(); |
| } |
| |
| @override |
| void visitCatchClause(CatchClause node) { |
| token(node.onKeyword, after: space); |
| visit(node.exceptionType); |
| |
| if (node.catchKeyword != null) { |
| if (node.exceptionType != null) { |
| space(); |
| } |
| token(node.catchKeyword); |
| space(); |
| token(node.leftParenthesis); |
| visit(node.exceptionParameter); |
| token(node.comma, after: space); |
| visit(node.stackTraceParameter); |
| token(node.rightParenthesis); |
| space(); |
| } else { |
| space(); |
| } |
| visit(node.body); |
| } |
| |
| @override |
| visitCatchClauseParameter(CatchClauseParameter node) { |
| token(node.name); |
| } |
| |
| @override |
| void visitClassDeclaration(ClassDeclaration node) { |
| visitMetadata(node.metadata); |
| |
| builder.nestExpression(); |
| modifier(node.abstractKeyword); |
| modifier(node.baseKeyword); |
| modifier(node.interfaceKeyword); |
| modifier(node.finalKeyword); |
| modifier(node.sealedKeyword); |
| modifier(node.mixinKeyword); |
| modifier(node.inlineKeyword); |
| token(node.classKeyword); |
| space(); |
| token(node.name); |
| visit(node.typeParameters); |
| visit(node.extendsClause); |
| _visitClauses(node.withClause, node.implementsClause); |
| visit(node.nativeClause, before: space); |
| space(); |
| |
| builder.unnest(); |
| _visitBody(node.leftBracket, node.members, node.rightBracket); |
| } |
| |
| @override |
| void visitClassTypeAlias(ClassTypeAlias node) { |
| visitMetadata(node.metadata); |
| |
| _simpleStatement(node, () { |
| modifier(node.abstractKeyword); |
| modifier(node.baseKeyword); |
| modifier(node.interfaceKeyword); |
| modifier(node.finalKeyword); |
| modifier(node.sealedKeyword); |
| modifier(node.mixinKeyword); |
| token(node.typedefKeyword); |
| space(); |
| token(node.name); |
| visit(node.typeParameters); |
| space(); |
| token(node.equals); |
| space(); |
| |
| visit(node.superclass); |
| _visitClauses(node.withClause, node.implementsClause); |
| }); |
| } |
| |
| @override |
| void visitComment(Comment node) {} |
| |
| @override |
| void visitCommentReference(CommentReference node) {} |
| |
| @override |
| void visitCompilationUnit(CompilationUnit node) { |
| visit(node.scriptTag); |
| |
| // Put a blank line between the library tag and the other directives. |
| Iterable<Directive> directives = node.directives; |
| if (directives.isNotEmpty && directives.first is LibraryDirective) { |
| visit(directives.first); |
| twoNewlines(); |
| |
| directives = directives.skip(1); |
| } |
| |
| visitNodes(directives, between: oneOrTwoNewlines); |
| |
| var needsDouble = true; |
| 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) needsDouble = true; |
| |
| if (needsDouble) { |
| twoNewlines(); |
| } else { |
| // Variables and arrow-bodied members can be more tightly packed if |
| // the user wants to group things together. |
| oneOrTwoNewlines(); |
| } |
| |
| visit(declaration); |
| |
| needsDouble = false; |
| if (hasBody) { |
| // Add a blank line after types declarations with bodies. |
| needsDouble = true; |
| } else if (declaration is FunctionDeclaration) { |
| // Add a blank line after non-empty block functions. |
| var body = declaration.functionExpression.body; |
| if (body is BlockFunctionBody) { |
| needsDouble = body.block.statements.isNotEmpty; |
| } |
| } |
| } |
| } |
| |
| @override |
| void visitConditionalExpression(ConditionalExpression node) { |
| // TODO(rnystrom): Consider revisiting whether users prefer this after 2.13. |
| /* |
| // Flatten else-if style chained conditionals. |
| var shouldNest = node.parent is! ConditionalExpression || |
| (node.parent as ConditionalExpression).elseExpression != node; |
| if (shouldNest) builder.nestExpression(); |
| */ |
| |
| // Start lazily so we don't force the operator to split if a line comment |
| // appears before the first operand. If we split after one clause in a |
| // conditional, always split after both. |
| builder.startLazyRule(ConditionalRule()); |
| |
| // TODO: Hack. Figure out better way to handle conditional expressions. |
| // Indent the conditional expression +4 leading up to the "?" and ":" and |
| // then another +2 so that block bodies inside the operands are aligned |
| // past the punctuation. |
| builder.nestExpression( |
| indent: (node.parent is ConditionalExpression |
| ? Indent.block |
| : Indent.expression) + |
| Indent.block); |
| builder.startBlockArgumentNesting(); |
| |
| visit(node.condition); |
| |
| split(); |
| token(node.question); |
| space(); |
| visit(node.thenExpression); |
| |
| split(); |
| token(node.colon); |
| space(); |
| visit(node.elseExpression); |
| |
| builder.endBlockArgumentNesting(); |
| |
| builder.unnest(); |
| |
| // If conditional expressions are directly nested, force them all to split. |
| // This line here forces the child, which implicitly forces the surrounding |
| // parent rules to split too. |
| if (node.parent is ConditionalExpression) builder.forceRules(); |
| |
| builder.endRule(); |
| |
| // TODO(rnystrom): Consider revisiting whether users prefer this after 2.13. |
| /* |
| if (shouldNest) builder.unnest(); |
| */ |
| } |
| |
| @override |
| void visitConfiguration(Configuration node) { |
| token(node.ifKeyword); |
| space(); |
| token(node.leftParenthesis); |
| visit(node.name); |
| |
| if (node.equalToken != null) { |
| builder.nestExpression(); |
| space(); |
| token(node.equalToken); |
| soloSplit(); |
| visit(node.value); |
| builder.unnest(); |
| } |
| |
| token(node.rightParenthesis); |
| space(); |
| visit(node.uri); |
| } |
| |
| @override |
| void visitConstantPattern(ConstantPattern node) { |
| token(node.constKeyword, after: space); |
| visit(node.expression); |
| } |
| |
| @override |
| void visitConstructorDeclaration(ConstructorDeclaration node) { |
| visitMetadata(node.metadata); |
| |
| modifier(node.externalKeyword); |
| modifier(node.constKeyword); |
| modifier(node.factoryKeyword); |
| visit(node.returnType); |
| token(node.period); |
| token(node.name); |
| |
| // Make the rule for the ":" span both the preceding parameter list and |
| // the entire initialization list. This ensures that we split before the |
| // ":" if the parameters and initialization list don't all fit on one line. |
| if (node.initializers.isNotEmpty) builder.startRule(); |
| |
| // TODO: Still relevant for Flutter style? |
| // If the redirecting constructor happens to wrap, we want to make sure |
| // the parameter list gets more deeply indented. |
| if (node.redirectedConstructor != null) builder.nestExpression(); |
| |
| _visitFunctionBody(null, node.parameters, node.body, (parameterRule) { |
| // Check for redirects or initializer lists. |
| if (node.redirectedConstructor != null) { |
| _visitConstructorRedirects(node); |
| builder.unnest(); |
| } else if (node.initializers.isNotEmpty) { |
| _visitConstructorInitializers(node, parameterRule); |
| |
| // End the rule for ":" after all of the initializers. |
| builder.endRule(); |
| } |
| }); |
| } |
| |
| void _visitConstructorRedirects(ConstructorDeclaration node) { |
| token(node.separator /* = */, before: space); |
| soloSplit(); |
| visitCommaSeparatedNodes(node.initializers); |
| visit(node.redirectedConstructor); |
| } |
| |
| void _visitConstructorInitializers( |
| ConstructorDeclaration node, Rule? parameterRule) { |
| var initializerRule = InitializerRule(parameterRule, |
| hasRightDelimiter: node.parameters.rightDelimiter != null); |
| |
| builder.startRule(initializerRule); |
| builder.nestExpression(indent: 0, now: true, rule: initializerRule); |
| builder.startBlockArgumentNesting(); |
| |
| // ":". |
| // TODO: This does the wrong think if there is a line comment before the |
| // "//". |
| initializerRule.bindColon(split()); |
| token(node.separator); |
| space(); |
| |
| for (var i = 0; i < node.initializers.length; i++) { |
| if (i > 0) { |
| // Preceding comma. |
| token(node.initializers[i].beginToken.previous); |
| split(); |
| } |
| |
| node.initializers[i].accept(this); |
| } |
| |
| builder.endBlockArgumentNesting(); |
| builder.unnest(); |
| builder.endRule(); |
| } |
| |
| @override |
| void visitConstructorFieldInitializer(ConstructorFieldInitializer node) { |
| builder.nestExpression(); |
| |
| token(node.thisKeyword); |
| token(node.period); |
| visit(node.fieldName); |
| |
| _visitAssignment(node.equals, node.expression); |
| |
| builder.unnest(); |
| } |
| |
| @override |
| void visitConstructorName(ConstructorName node) { |
| visit(node.type); |
| token(node.period); |
| visit(node.name); |
| } |
| |
| @override |
| void visitContinueStatement(ContinueStatement node) { |
| _simpleStatement(node, () { |
| token(node.continueKeyword); |
| visit(node.label, before: space); |
| }); |
| } |
| |
| @override |
| void visitDeclaredIdentifier(DeclaredIdentifier node) { |
| modifier(node.keyword); |
| visit(node.type, after: space); |
| token(node.name); |
| } |
| |
| @override |
| void visitDeclaredVariablePattern(DeclaredVariablePattern node) { |
| _visitVariablePattern(node.keyword, node.type, node.name); |
| } |
| |
| @override |
| void visitDefaultFormalParameter(DefaultFormalParameter node) { |
| visit(node.parameter); |
| if (node.separator case var separator?) { |
| builder.startSpan(); |
| builder.nestExpression(); |
| |
| if (_formatter.fixes.contains(StyleFix.namedDefaultSeparator)) { |
| // Change the separator to "=". |
| space(); |
| writePrecedingCommentsAndNewlines(separator); |
| _writeText('=', separator); |
| } else { |
| // The '=' separator is preceded by a space, ":" is not. |
| if (separator.type == TokenType.EQ) space(); |
| token(separator); |
| } |
| |
| // TODO: Needs tests. |
| // If the expression has a delimited body, prefer to split the body instead |
| // of at the `=`. Prefer: |
| // |
| // var x = foo( |
| // argument, |
| // ); |
| // |
| // Over: |
| // |
| // var x = |
| // foo(argument); |
| soloSplit(node.defaultValue!.isDelimitedOrCall |
| ? Cost.assignDelimited |
| : Cost.assign); |
| visit(node.defaultValue!); |
| |
| builder.unnest(); |
| builder.endSpan(); |
| } |
| } |
| |
| @override |
| void visitDoStatement(DoStatement node) { |
| builder.nestExpression(); |
| token(node.doKeyword); |
| space(); |
| builder.unnest(now: false); |
| visit(node.body); |
| |
| builder.nestExpression(); |
| space(); |
| token(node.whileKeyword); |
| space(); |
| token(node.leftParenthesis); |
| soloZeroSplit(); |
| visit(node.condition); |
| token(node.rightParenthesis); |
| token(node.semicolon); |
| builder.unnest(); |
| } |
| |
| @override |
| void visitDottedName(DottedName node) { |
| for (var component in node.components) { |
| // Write the preceding ".". |
| if (component != node.components.first) { |
| token(component.beginToken.previous); |
| } |
| |
| visit(component); |
| } |
| } |
| |
| @override |
| void visitDoubleLiteral(DoubleLiteral node) { |
| token(node.literal); |
| } |
| |
| @override |
| void visitEmptyFunctionBody(EmptyFunctionBody node) { |
| token(node.semicolon); |
| } |
| |
| @override |
| void visitEmptyStatement(EmptyStatement node) { |
| token(node.semicolon); |
| } |
| |
| @override |
| void visitEnumConstantDeclaration(EnumConstantDeclaration node) { |
| visitMetadata(node.metadata); |
| token(node.name); |
| |
| var arguments = node.arguments; |
| if (arguments != null) { |
| builder.nestExpression(); |
| visit(arguments.typeArguments); |
| |
| var constructor = arguments.constructorSelector; |
| if (constructor != null) { |
| token(constructor.period); |
| visit(constructor.name); |
| } |
| |
| visitArgumentList(arguments.argumentList); |
| builder.unnest(); |
| } |
| } |
| |
| @override |
| void visitEnumDeclaration(EnumDeclaration node) { |
| visitMetadata(node.metadata); |
| |
| builder.nestExpression(); |
| token(node.enumKeyword); |
| space(); |
| token(node.name); |
| visit(node.typeParameters); |
| _visitClauses(node.withClause, node.implementsClause); |
| space(); |
| |
| builder.unnest(); |
| |
| _beginBody(node.leftBracket, space: true); |
| |
| // The ";" after the constants, which may occur after a trailing comma. |
| Token afterConstants = node.constants.last.endToken.next!; |
| Token? semicolon; |
| if (afterConstants.type == TokenType.SEMICOLON) { |
| semicolon = node.constants.last.endToken.next!; |
| } else if (afterConstants.type == TokenType.COMMA && |
| afterConstants.next!.type == TokenType.SEMICOLON) { |
| semicolon = afterConstants.next!; |
| } |
| |
| for (var value in node.constants) { |
| if (value != node.constants.first) splitOrTwoNewlines(); |
| |
| visit(value); |
| |
| // Don't add a trailing comma if there will be a ";" at the end of the |
| // last value. |
| // TODO: Preserve comment on erased trailing comma with semicolon after |
| // it. |
| // TODO: Should probably discard semicolon (and replace with trailing |
| // comma as needed) if there are no members. |
| if (value != node.constants.last || semicolon == null) { |
| writeCommaAfter(value, isTrailing: value == node.constants.last); |
| } |
| } |
| |
| if (semicolon != null) { |
| token(semicolon); |
| |
| // Put a blank line between the constants and members. |
| if (node.members.isNotEmpty) twoNewlines(); |
| } |
| |
| _visitBodyContents(node.members); |
| |
| _endBody(node.rightBracket, |
| forceSplit: semicolon != null || |
| _preserveTrailingCommaAfter(node.constants.last)); |
| } |
| |
| @override |
| void visitExportDirective(ExportDirective node) { |
| _visitDirectiveMetadata(node); |
| _simpleStatement(node, () { |
| token(node.exportKeyword); |
| space(); |
| visit(node.uri); |
| |
| _visitConfigurations(node.configurations); |
| _visitCombinators(node.combinators); |
| }); |
| } |
| |
| @override |
| void visitExpressionFunctionBody(ExpressionFunctionBody node) { |
| // Space after the parameter list. |
| space(); |
| |
| // The "async" or "sync" keyword and "*". |
| token(node.keyword); |
| token(node.star); |
| if (node.keyword != null || node.star != null) space(); |
| |
| token(node.functionDefinition); // "=>". |
| |
| if (node.expression.isDelimitedOrCall) { |
| // Don't allow a split between `=>` and a collection. Instead, we want |
| // the collection itself to split. |
| // TODO: Write tests for this. |
| space(); |
| } else { |
| // Split after the "=>". |
| builder.nestExpression(now: true); |
| builder.startRule(Rule(Cost.arrow)); |
| split(); |
| } |
| |
| // TODO: See if the logic around binary operators can be simplified. |
| // If the body is a binary operator expression, then we want to force the |
| // split at `=>` if the operators split. See visitBinaryExpression(). |
| if (node.expression is! BinaryExpression && |
| !node.expression.isDelimitedOrCall) { |
| builder.endRule(); |
| } |
| |
| builder.startBlockArgumentNesting(); |
| visit(node.expression); |
| builder.endBlockArgumentNesting(); |
| |
| if (!node.expression.isDelimitedOrCall) { |
| builder.unnest(); |
| } |
| |
| if (node.expression is BinaryExpression && |
| !node.expression.isDelimitedOrCall) { |
| builder.endRule(); |
| } |
| |
| token(node.semicolon); |
| } |
| |
| /// Parenthesize the target of the given statement's expression (assumed to |
| /// be a CascadeExpression) before removing the cascade. |
| void _fixCascadeByParenthesizingTarget(ExpressionStatement statement) { |
| var cascade = statement.expression as CascadeExpression; |
| assert(cascade.cascadeSections.length == 1); |
| |
| // Write any leading comments and whitespace immediately, as they should |
| // precede the new opening parenthesis, but then prevent them from being |
| // written again after the parenthesis. |
| writePrecedingCommentsAndNewlines(cascade.target.beginToken); |
| _suppressPrecedingCommentsAndNewLines.add(cascade.target.beginToken); |
| |
| // Finally, we can revisit a clone of this ExpressionStatement to actually |
| // remove the cascade. |
| visit( |
| fixCascadeByParenthesizingTarget( |
| expressionStatement: statement, |
| cascadeExpression: cascade, |
| ), |
| ); |
| } |
| |
| void _removeCascade(ExpressionStatement statement) { |
| var cascade = statement.expression as CascadeExpression; |
| var subexpression = cascade.cascadeSections.single; |
| builder.nestExpression(); |
| |
| if (subexpression is AssignmentExpression || |
| subexpression is MethodInvocation || |
| subexpression is PropertyAccess) { |
| // CascadeExpression("leftHandSide", "..", |
| // AssignmentExpression("target", "=", "rightHandSide")) |
| // |
| // transforms to |
| // |
| // AssignmentExpression( |
| // PropertyAccess("leftHandSide", ".", "target"), |
| // "=", |
| // "rightHandSide") |
| // |
| // CascadeExpression("leftHandSide", "..", |
| // MethodInvocation("target", ".", "methodName", ...)) |
| // |
| // transforms to |
| // |
| // MethodInvocation( |
| // PropertyAccess("leftHandSide", ".", "target"), |
| // ".", |
| // "methodName", ...) |
| // |
| // And similarly for PropertyAccess expressions. |
| visit(insertCascadeTargetIntoExpression( |
| expression: subexpression, cascadeTarget: cascade.target)); |
| } else { |
| throw UnsupportedError( |
| '--fix-single-cascade-statements: subexpression of cascade ' |
| '"$cascade" has unsupported type ${subexpression.runtimeType}.'); |
| } |
| |
| token(statement.semicolon); |
| builder.unnest(); |
| } |
| |
| /// Remove any unnecessary single cascade from the given expression statement, |
| /// which is assumed to contain a [CascadeExpression]. |
| /// |
| /// Returns true after applying the fix, which involves visiting the nested |
| /// expression. Callers must visit the nested expression themselves |
| /// if-and-only-if this method returns false. |
| bool _fixSingleCascadeStatement(ExpressionStatement statement) { |
| var cascade = statement.expression as CascadeExpression; |
| if (cascade.cascadeSections.length != 1) return false; |
| |
| var target = cascade.target; |
| if (target is AsExpression || |
| target is AwaitExpression || |
| target is BinaryExpression || |
| target is ConditionalExpression || |
| target is IsExpression || |
| target is PostfixExpression || |
| target is PrefixExpression) { |
| // In these cases, the cascade target needs to be parenthesized before |
| // removing the cascade, otherwise the semantics will change. |
| _fixCascadeByParenthesizingTarget(statement); |
| return true; |
| } else if (target is BooleanLiteral || |
| target is FunctionExpression || |
| target is IndexExpression || |
| target is InstanceCreationExpression || |
| target is IntegerLiteral || |
| target is ListLiteral || |
| target is NullLiteral || |
| target is MethodInvocation || |
| target is ParenthesizedExpression || |
| target is PrefixedIdentifier || |
| target is PropertyAccess || |
| target is SimpleIdentifier || |
| target is StringLiteral || |
| target is ThisExpression) { |
| // OK to simply remove the cascade. |
| _removeCascade(statement); |
| return true; |
| } else { |
| // If we get here, some new syntax was added to the language that the fix |
| // does not yet support. Leave it as is. |
| return false; |
| } |
| } |
| |
| @override |
| void visitExpressionStatement(ExpressionStatement node) { |
| if (_formatter.fixes.contains(StyleFix.singleCascadeStatements) && |
| node.expression is CascadeExpression && |
| _fixSingleCascadeStatement(node)) { |
| return; |
| } |
| |
| _simpleStatement(node, () { |
| visit(node.expression); |
| }); |
| } |
| |
| @override |
| void visitExtendsClause(ExtendsClause node) { |
| // If a type argument in the supertype splits, then split before "extends" |
| // too. |
| builder.startRule(); |
| builder.startBlockArgumentNesting(); |
| split(); |
| |
| token(node.extendsKeyword); |
| space(); |
| visit(node.superclass); |
| |
| builder.endBlockArgumentNesting(); |
| builder.endRule(); |
| } |
| |
| @override |
| void visitExtensionDeclaration(ExtensionDeclaration node) { |
| visitMetadata(node.metadata); |
| |
| builder.nestExpression(); |
| token(node.extensionKeyword); |
| |
| // Don't put a space after `extension` if the extension is unnamed. That |
| // way, generic unnamed extensions format like `extension<T> on ...`. |
| token(node.name, before: space); |
| |
| visit(node.typeParameters); |
| |
| // If a type argument in the on type splits, then split before "on" too. |
| builder.startRule(); |
| builder.startBlockArgumentNesting(); |
| |
| split(); |
| token(node.onKeyword); |
| space(); |
| visit(node.extendedType); |
| |
| builder.endBlockArgumentNesting(); |
| builder.endRule(); |
| |
| space(); |
| builder.unnest(); |
| _visitBody(node.leftBracket, node.members, node.rightBracket); |
| } |
| |
| @override |
| void visitFieldDeclaration(FieldDeclaration node) { |
| visitMetadata(node.metadata); |
| |
| modifier(node.externalKeyword); |
| modifier(node.staticKeyword); |
| modifier(node.abstractKeyword); |
| modifier(node.covariantKeyword); |
| visit(node.fields); |
| token(node.semicolon); |
| } |
| |
| @override |
| void visitFieldFormalParameter(FieldFormalParameter node) { |
| _visitParameterMetadata(node.metadata, () { |
| _beginFormalParameter(node); |
| token(node.keyword, after: space); |
| visit(node.type, after: split); |
| token(node.thisKeyword); |
| token(node.period); |
| token(node.name); |
| visit(node.parameters); |
| token(node.question); |
| _endFormalParameter(node); |
| }); |
| } |
| |
| @override |
| Rule? visitFormalParameterList(FormalParameterList node, |
| {bool nestExpression = true}) { |
| // Corner case: empty parameter lists. |
| if (node.parameters.isEmpty) { |
| token(node.leftParenthesis); |
| |
| // If there is a comment, do allow splitting before it. |
| if (node.rightParenthesis.precedingComments != null) soloZeroSplit(); |
| |
| token(node.rightParenthesis); |
| return null; |
| } |
| |
| token(node.leftParenthesis); |
| |
| var parameterRule = Rule(Cost.parameterList); |
| builder.startRule(parameterRule); |
| |
| // Find the parameter immediately preceding the optional parameters (if |
| // there are any). |
| FormalParameter? lastRequired; |
| for (var i = 0; i < node.parameters.length; i++) { |
| if (node.parameters[i] is DefaultFormalParameter) { |
| if (i > 0) lastRequired = node.parameters[i - 1]; |
| break; |
| } |
| } |
| |
| // If all parameters are optional, put the "[" or "{" right after "(". |
| if (lastRequired == null) { |
| token(node.leftDelimiter); |
| } |
| |
| // Process the parameters as a separate set of chunks. |
| builder = builder.startBlock(); |
| |
| var spaceWhenUnsplit = true; |
| for (var parameter in node.parameters) { |
| builder.split(space: spaceWhenUnsplit); |
| visit(parameter); |
| writeCommaAfter(parameter, isTrailing: parameter == node.parameters.last); |
| |
| // If the optional parameters start after this one, put the delimiter |
| // at the end of its line. If we don't split, don't put a space after |
| // the delimiter. |
| spaceWhenUnsplit = parameter != lastRequired; |
| if (parameter == lastRequired) { |
| space(); |
| token(node.leftDelimiter); |
| lastRequired = null; |
| } |
| } |
| |
| // Put comments before the closing ")", "]", or "}" inside the block. |
| var firstDelimiter = node.rightDelimiter ?? node.rightParenthesis; |
| if (firstDelimiter.precedingComments != null) { |
| writePrecedingCommentsAndNewlines(firstDelimiter); |
| } |
| |
| builder = builder.endBlock( |
| forceSplit: _preserveTrailingCommaAfter(node.parameters.last)); |
| builder.endRule(); |
| |
| // Now write the delimiter itself. |
| _writeText(firstDelimiter.lexeme, firstDelimiter); |
| if (firstDelimiter != node.rightParenthesis) { |
| token(node.rightParenthesis); |
| } |
| |
| return parameterRule; |
| } |
| |
| @override |
| void visitForElement(ForElement node) { |
| // Treat a spread of a collection literal like a block in a for statement |
| // and don't split after the for parts. |
| var isSpreadBody = node.body.isSpreadCollection; |
| |
| builder.nestExpression(); |
| token(node.awaitKeyword, after: space); |
| token(node.forKeyword); |
| space(); |
| token(node.leftParenthesis); |
| |
| // Start the body rule so that if the parts split, the body does too. |
| builder.startRule(); |
| |
| // The rule for the parts. |
| builder.startRule(); |
| visit(node.forLoopParts); |
| token(node.rightParenthesis); |
| builder.endRule(); |
| builder.unnest(); |
| |
| builder.nestExpression(indent: Indent.block, now: true); |
| |
| if (isSpreadBody) { |
| space(); |
| } else { |
| split(); |
| |
| // If the body is a non-spread collection or lambda, indent it. |
| builder.startBlockArgumentNesting(); |
| } |
| |
| visit(node.body); |
| |
| if (!isSpreadBody) builder.endBlockArgumentNesting(); |
| builder.unnest(); |
| |
| // If a control flow element is nested inside another, force the outer one |
| // to split. |
| if (node.body.isControlFlowElement) builder.forceRules(); |
| |
| builder.endRule(); |
| } |
| |
| @override |
| void visitForStatement(ForStatement node) { |
| builder.nestExpression(); |
| token(node.awaitKeyword, after: space); |
| token(node.forKeyword); |
| space(); |
| token(node.leftParenthesis); |
| |
| builder.startRule(); |
| |
| visit(node.forLoopParts); |
| |
| token(node.rightParenthesis); |
| builder.endRule(); |
| builder.unnest(); |
| |
| _visitLoopBody(node.body); |
| } |
| |
| @override |
| void visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) { |
| // TODO(rnystrom): The formatting logic here is slightly different from |
| // how parameter metadata is handled and from how variable metadata is |
| // handled. I think what it does works better in the context of a for-in |
| // loop, but consider trying to unify this with one of the above. |
| // |
| // Metadata on class and variable declarations is *always* split: |
| // |
| // @foo |
| // class Bar {} |
| // |
| // Metadata on parameters has some complex logic to handle multiple |
| // parameters with metadata. It also indents the parameters farther than |
| // the metadata when split: |
| // |
| // function( |
| // @foo(long arg list...) |
| // parameter1, |
| // @foo |
| // parameter2) {} |
| // |
| // For for-in variables, we allow it to not split, like parameters, but |
| // don't indent the variable when it does split: |
| // |
| // for ( |
| // @foo |
| // @bar |
| // var blah in stuff) {} |
| // TODO(rnystrom): we used to call builder.startRule() here, but now we call |
| // it from visitForStatement2 prior to the `(`. Is that ok? |
| builder.nestExpression(); |
| builder.startBlockArgumentNesting(); |
| |
| visitNodes(node.loopVariable.metadata, between: split, after: split); |
| visit(node.loopVariable); |
| // TODO(rnystrom): we used to call builder.endRule() here, but now we call |
| // it from visitForStatement2 after the `)`. Is that ok? |
| |
| _visitForEachPartsFromIn(node); |
| |
| builder.endBlockArgumentNesting(); |
| builder.unnest(); |
| } |
| |
| void _visitForEachPartsFromIn(ForEachParts node) { |
| soloSplit(); |
| token(node.inKeyword); |
| space(); |
| visit(node.iterable); |
| } |
| |
| @override |
| void visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) { |
| visit(node.identifier); |
| _visitForEachPartsFromIn(node); |
| } |
| |
| @override |
| void visitForEachPartsWithPattern(ForEachPartsWithPattern node) { |
| builder.startBlockArgumentNesting(); |
| visitNodes(node.metadata, between: split, after: split); |
| token(node.keyword); |
| space(); |
| visit(node.pattern); |
| builder.endBlockArgumentNesting(); |
| _visitForEachPartsFromIn(node); |
| } |
| |
| @override |
| void visitForPartsWithDeclarations(ForPartsWithDeclarations node) { |
| // Nest split variables more so they aren't at the same level |
| // as the rest of the loop clauses. |
| builder.startBlockArgumentNesting(); |
| builder.nestExpression(); |
| |
| // Allow the variables to stay unsplit even if the clauses split. |
| builder.startRule(); |
| |
| var declaration = node.variables; |
| visitNodes(declaration.metadata, between: split, after: split); |
| modifier(declaration.keyword); |
| visit(declaration.type, after: space); |
| |
| visitCommaSeparatedNodes(declaration.variables, between: () { |
| split(); |
| }); |
| |
| builder.endRule(); |
| builder.endBlockArgumentNesting(); |
| builder.unnest(); |
| |
| _visitForPartsFromLeftSeparator(node); |
| } |
| |
| @override |
| void visitForPartsWithExpression(ForPartsWithExpression node) { |
| visit(node.initialization); |
| _visitForPartsFromLeftSeparator(node); |
| } |
| |
| @override |
| void visitForPartsWithPattern(ForPartsWithPattern node) { |
| builder.startBlockArgumentNesting(); |
| builder.nestExpression(); |
| |
| var declaration = node.variables; |
| visitNodes(declaration.metadata, between: split, after: split); |
| token(declaration.keyword); |
| space(); |
| visit(declaration.pattern); |
| _visitAssignment(declaration.equals, declaration.expression); |
| |
| builder.unnest(); |
| builder.endBlockArgumentNesting(); |
| |
| _visitForPartsFromLeftSeparator(node); |
| } |
| |
| void _visitForPartsFromLeftSeparator(ForParts node) { |
| token(node.leftSeparator); |
| |
| // The condition clause. |
| if (node.condition != null) split(); |
| visit(node.condition); |
| token(node.rightSeparator); |
| |
| // The update clause. |
| if (node.updaters.isNotEmpty) { |
| split(); |
| |
| // Allow the updates to stay unsplit even if the clauses split. |
| builder.startRule(); |
| |
| visitCommaSeparatedNodes(node.updaters, between: split); |
| |
| builder.endRule(); |
| } |
| } |
| |
| @override |
| void visitFunctionDeclaration(FunctionDeclaration node) { |
| _visitFunctionOrMethodDeclaration( |
| metadata: node.metadata, |
| externalKeyword: node.externalKeyword, |
| propertyKeyword: node.propertyKeyword, |
| modifierKeyword: null, |
| operatorKeyword: null, |
| name: node.name, |
| returnType: node.returnType, |
| typeParameters: node.functionExpression.typeParameters, |
| formalParameters: node.functionExpression.parameters, |
| body: node.functionExpression.body, |
| ); |
| } |
| |
| @override |
| void visitFunctionDeclarationStatement(FunctionDeclarationStatement node) { |
| visit(node.functionDeclaration); |
| } |
| |
| @override |
| void visitFunctionExpression(FunctionExpression node) { |
| // Inside a function body is no longer in the surrounding const context. |
| var oldConstNesting = _constNesting; |
| _constNesting = 0; |
| |
| _visitFunctionBody(node.typeParameters, node.parameters, node.body); |
| |
| _constNesting = oldConstNesting; |
| } |
| |
| @override |
| void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { |
| // Try to keep the entire invocation one line. |
| builder.startSpan(); |
| builder.nestExpression(); |
| |
| visit(node.function); |
| visit(node.typeArguments); |
| visitArgumentList(node.argumentList); |
| |
| builder.unnest(); |
| builder.endSpan(); |
| } |
| |
| @override |
| void visitFunctionReference(FunctionReference node) { |
| visit(node.function); |
| visit(node.typeArguments); |
| } |
| |
| @override |
| void visitFunctionTypeAlias(FunctionTypeAlias node) { |
| visitMetadata(node.metadata); |
| |
| if (_formatter.fixes.contains(StyleFix.functionTypedefs)) { |
| _simpleStatement(node, () { |
| // Inlined visitGenericTypeAlias |
| _visitGenericTypeAliasHeader( |
| node.typedefKeyword, |
| node.name, |
| node.typeParameters, |
| null, |
| node.returnType?.beginToken ?? node.name); |
| |
| space(); |
| |
| // Recursively convert function-arguments to Function syntax. |
| _insideNewTypedefFix = true; |
| _visitGenericFunctionType( |
| node.returnType, null, node.name, null, node.parameters); |
| _insideNewTypedefFix = false; |
| }); |
| return; |
| } |
| |
| _simpleStatement(node, () { |
| token(node.typedefKeyword); |
| space(); |
| visit(node.returnType, after: space); |
| token(node.name); |
| visit(node.typeParameters); |
| visit(node.parameters); |
| }); |
| } |
| |
| @override |
| void visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) { |
| _visitParameterMetadata(node.metadata, () { |
| if (!_insideNewTypedefFix) { |
| modifier(node.requiredKeyword); |
| modifier(node.covariantKeyword); |
| visit(node.returnType, after: space); |
| // Try to keep the function's parameters with its name. |
| builder.startSpan(); |
| token(node.name); |
| _visitParameterSignature(node.typeParameters, node.parameters); |
| token(node.question); |
| builder.endSpan(); |
| } else { |
| _beginFormalParameter(node); |
| _visitGenericFunctionType(node.returnType, null, node.name, |
| node.typeParameters, node.parameters); |
| token(node.question); |
| split(); |
| token(node.name); |
| _endFormalParameter(node); |
| } |
| }); |
| } |
| |
| @override |
| void visitGenericFunctionType(GenericFunctionType node) { |
| _visitGenericFunctionType(node.returnType, node.functionKeyword, null, |
| node.typeParameters, node.parameters); |
| token(node.question); |
| } |
| |
| @override |
| void visitGenericTypeAlias(GenericTypeAlias node) { |
| visitNodes(node.metadata, between: newline, after: newline); |
| _simpleStatement(node, () { |
| _visitGenericTypeAliasHeader(node.typedefKeyword, node.name, |
| node.typeParameters, node.equals, null); |
| |
| space(); |
| |
| visit(node.type); |
| }); |
| } |
| |
| @override |
| void visitHideCombinator(HideCombinator node) { |
| _visitCombinator(node.keyword, node.hiddenNames); |
| } |
| |
| @override |
| void visitIfElement(IfElement node) { |
| // Treat a chain of if-else elements as a single unit so that we don't |
| // unnecessarily indent each subsequent section of the chain. |
| var ifElements = [ |
| for (CollectionElement? thisNode = node; |
| thisNode is IfElement; |
| thisNode = thisNode.elseElement) |
| thisNode |
| ]; |
| |
| // If the body of the then or else branch is a spread of a collection |
| // literal, then we want to format those collections more like blocks than |
| // like standalone objects. In particular, if both the then and else branch |
| // are spread collection literals, we want to ensure that they both split |
| // if either splits. So this: |
| // |
| // [ |
| // if (condition) ...[ |
| // thenClause |
| // ] else ...[ |
| // elseClause |
| // ] |
| // ] |
| // |
| // And not something like this: |
| // |
| // [ |
| // if (condition) ...[ |
| // thenClause |
| // ] else ...[elseClause] |
| // ] |
| // |
| // To do that, if we see that either clause is a spread collection, we |
| // create a single rule and force both collections to use it. |
| var spreadRule = Rule(); |
| var spreadBrackets = <CollectionElement, Token>{}; |
| for (var element in ifElements) { |
| var spreadBracket = element.thenElement.spreadCollectionBracket; |
| if (spreadBracket != null) { |
| spreadBrackets[element] = spreadBracket; |
| _bindBlockRule(spreadBracket, spreadRule); |
| } |
| } |
| |
| var elseSpreadBracket = |
| ifElements.last.elseElement?.spreadCollectionBracket; |
| if (elseSpreadBracket != null) { |
| spreadBrackets[ifElements.last.elseElement!] = elseSpreadBracket; |
| _bindBlockRule(elseSpreadBracket, spreadRule); |
| } |
| |
| void visitChild(CollectionElement element, CollectionElement child) { |
| builder.nestExpression(indent: 2, now: true); |
| |
| // Treat a spread of a collection literal like a block in an if statement |
| // and don't split after the "else". |
| var isSpread = spreadBrackets.containsKey(element); |
| if (isSpread) { |
| space(); |
| } else { |
| split(); |
| |
| // If the then clause is a non-spread collection or lambda, make sure the |
| // body is indented. |
| builder.startBlockArgumentNesting(); |
| } |
| |
| visit(child); |
| |
| if (!isSpread) builder.endBlockArgumentNesting(); |
| builder.unnest(); |
| } |
| |
| // Wrap the whole thing in a single rule. If a split happens inside the |
| // condition or the then clause, we want the then and else clauses to split. |
| builder.startLazyRule(); |
| |
| var hasInnerControlFlow = false; |
| for (var element in ifElements) { |
| _visitIfCondition(element.ifKeyword, element.leftParenthesis, |
| element.expression, element.caseClause, element.rightParenthesis); |
| |
| visitChild(element, element.thenElement); |
| if (element.thenElement.isControlFlowElement) { |
| hasInnerControlFlow = true; |
| } |
| |
| // Handle this element's "else" keyword and prepare to write the element, |
| // but don't write it. It will either be the next element in [ifElements] |
| // or the final else element handled after the loop. |
| if (element.elseElement != null) { |
| if (spreadBrackets.containsKey(element)) { |
| space(); |
| } else { |
| split(); |
| } |
| |
| token(element.elseKeyword); |
| |
| // If there is another if element in the chain, put a space between |
| // it and this "else". |
| if (element != ifElements.last) space(); |
| } |
| } |
| |
| // Handle the final trailing else if there is one. |
| var lastElse = ifElements.last.elseElement; |
| if (lastElse != null) { |
| visitChild(lastElse, lastElse); |
| |
| if (lastElse.isControlFlowElement) { |
| hasInnerControlFlow = true; |
| } |
| } |
| |
| // If a control flow element is nested inside another, force the outer one |
| // to split. |
| if (hasInnerControlFlow) builder.forceRules(); |
| builder.endRule(); |
| } |
| |
| @override |
| void visitIfStatement(IfStatement node) { |
| _visitIfCondition(node.ifKeyword, node.leftParenthesis, node.expression, |
| node.caseClause, node.rightParenthesis); |
| |
| void visitClause(Statement clause) { |
| if (clause is Block || clause is IfStatement) { |
| space(); |
| visit(clause); |
| } else { |
| // Allow splitting in a statement-bodied if even though it's against |
| // the style guide. Since we can't fix the code itself to follow the |
| // style guide, we should at least format it as well as we can. |
| builder.indent(); |
| builder.startRule(); |
| |
| // If there is an else clause, always split before both the then and |
| // else statements. |
| if (node.elseStatement != null) { |
| builder.writeNewline(); |
| } else { |
| builder.split(nest: false, space: true); |
| } |
| |
| visit(clause); |
| |
| builder.endRule(); |
| builder.unindent(); |
| } |
| } |
| |
| visitClause(node.thenStatement); |
| |
| if (node.elseStatement != null) { |
| if (node.thenStatement is Block) { |
| space(); |
| } else { |
| // Corner case where an else follows a single-statement then clause. |
| // This is against the style guide, but we still need to handle it. If |
| // it happens, put the else on the next line. |
| newline(); |
| } |
| |
| token(node.elseKeyword); |
| visitClause(node.elseStatement!); |
| } |
| } |
| |
| @override |
| void visitImplementsClause(ImplementsClause node) { |
| _visitCombinator(node.implementsKeyword, node.interfaces); |
| } |
| |
| @override |
| void visitImportDirective(ImportDirective node) { |
| _visitDirectiveMetadata(node); |
| _simpleStatement(node, () { |
| token(node.importKeyword); |
| space(); |
| visit(node.uri); |
| |
| _visitConfigurations(node.configurations); |
| |
| if (node.asKeyword != null) { |
| soloSplit(); |
| token(node.deferredKeyword, after: space); |
| token(node.asKeyword); |
| space(); |
| visit(node.prefix); |
| } |
| |
| _visitCombinators(node.combinators); |
| }); |
| } |
| |
| @override |
| void visitIndexExpression(IndexExpression node) { |
| builder.nestExpression(); |
| |
| if (node.isCascaded) { |
| token(node.period); |
| } else { |
| visit(node.target); |
| } |
| |
| finishIndexExpression(node); |
| |
| builder.unnest(); |
| } |
| |
| /// Visit the index part of [node], excluding the target. |
| /// |
| /// Called by [CallChainVisitor] to handle index expressions in the middle of |
| /// call chains. |
| void finishIndexExpression(IndexExpression node) { |
| if (node.target is IndexExpression) { |
| // Edge case: On a chain of [] accesses, allow splitting between them. |
| // Produces nicer output in cases like: |
| // |
| // someJson['property']['property']['property']['property']... |
| soloZeroSplit(); |
| } |
| |
| builder.startSpan(Cost.index); |
| token(node.question); |
| _beginBody(node.leftBracket); |
| zeroSplit(); |
| visit(node.index); |
| _endBody(node.rightBracket); |
| builder.endSpan(); |
| } |
| |
| @override |
| void visitInstanceCreationExpression(InstanceCreationExpression node) { |
| builder.startSpan(); |
| |
| var includeKeyword = true; |
| |
| if (node.keyword != null) { |
| if (node.keyword!.keyword == Keyword.NEW && |
| _formatter.fixes.contains(StyleFix.optionalNew)) { |
| includeKeyword = false; |
| } else if (node.keyword!.keyword == Keyword.CONST && |
| _formatter.fixes.contains(StyleFix.optionalConst) && |
| _constNesting > 0) { |
| includeKeyword = false; |
| } |
| } |
| |
| if (includeKeyword) { |
| token(node.keyword, after: space); |
| } else { |
| // Don't lose comments before the discarded keyword, if any. |
| writePrecedingCommentsAndNewlines(node.keyword!); |
| } |
| |
| builder.startSpan(Cost.constructorName); |
| |
| // Start the expression nesting for the argument list here, in case this |
| // is a generic constructor with type arguments. If it is, we need the type |
| // arguments to be nested too so they get indented past the arguments. |
| builder.nestExpression(); |
| visit(node.constructorName); |
| |
| _startPossibleConstContext(node.keyword); |
| |
| builder.endSpan(); |
| visitArgumentList(node.argumentList); |
| builder.endSpan(); |
| |
| _endPossibleConstContext(node.keyword); |
| |
| builder.unnest(); |
| } |
| |
| @override |
| void visitIntegerLiteral(IntegerLiteral node) { |
| token(node.literal); |
| } |
| |
| @override |
| void visitInterpolationExpression(InterpolationExpression node) { |
| builder.preventSplit(); |
| token(node.leftBracket); |
| builder.startSpan(); |
| visit(node.expression); |
| builder.endSpan(); |
| token(node.rightBracket); |
| builder.endPreventSplit(); |
| } |
| |
| @override |
| void visitInterpolationString(InterpolationString node) { |
| _writeStringLiteral(node.contents); |
| } |
| |
| @override |
| void visitIsExpression(IsExpression node) { |
| builder.startSpan(); |
| builder.nestExpression(); |
| visit(node.expression); |
| soloSplit(); |
| token(node.isOperator); |
| token(node.notOperator); |
| space(); |
| visit(node.type); |
| builder.unnest(); |
| builder.endSpan(); |
| } |
| |
| @override |
| void visitLabel(Label node) { |
| visit(node.label); |
| token(node.colon); |
| } |
| |
| @override |
| void visitLabeledStatement(LabeledStatement node) { |
| _visitLabels(node.labels); |
| visit(node.statement); |
| } |
| |
| @override |
| void visitLibraryDirective(LibraryDirective node) { |
| _visitDirectiveMetadata(node); |
| _simpleStatement(node, () { |
| token(node.libraryKeyword); |
| if (node.name2 != null) { |
| visit(node.name2, before: space); |
| } |
| }); |
| } |
| |
| @override |
| void visitLibraryIdentifier(LibraryIdentifier node) { |
| visit(node.components.first); |
| for (var component in node.components.skip(1)) { |
| token(component.beginToken.previous); // "." |
| visit(component); |
| } |
| } |
| |
| @override |
| void visitListLiteral(ListLiteral node) { |
| // Corner case: Splitting inside a list looks bad if there's only one |
| // element, so make those more costly. |
| var cost = node.elements.length <= 1 ? Cost.singleElementList : Cost.normal; |
| _visitCollectionLiteral(node.leftBracket, node.elements, node.rightBracket, |
| constKeyword: node.constKeyword, |
| typeArguments: node.typeArguments, |
| splitOuterCollection: true, |
| cost: cost); |
| } |
| |
| @override |
| void visitListPattern(ListPattern node) { |
| _visitCollectionLiteral( |
| node.leftBracket, |
| node.elements, |
| node.rightBracket, |
| typeArguments: node.typeArguments, |
| ); |
| } |
| |
| @override |
| void visitLogicalAndPattern(LogicalAndPattern node) { |
| _visitBinary<LogicalAndPattern>( |
| node, |
| (pattern) => BinaryNode( |
| pattern.leftOperand, pattern.operator, pattern.rightOperand)); |
| } |
| |
| @override |
| void visitLogicalOrPattern(LogicalOrPattern node) { |
| _visitBinary<LogicalOrPattern>( |
| node, |
| (pattern) => BinaryNode( |
| pattern.leftOperand, pattern.operator, pattern.rightOperand)); |
| } |
| |
| @override |
| void visitMapLiteralEntry(MapLiteralEntry node) { |
| builder.nestExpression(); |
| visit(node.key); |
| token(node.separator); |
| soloSplit(); |
| visit(node.value); |
| builder.unnest(); |
| } |
| |
| @override |
| void visitMapPattern(MapPattern node) { |
| _visitCollectionLiteral(node.leftBracket, node.elements, node.rightBracket, |
| typeArguments: node.typeArguments); |
| } |
| |
| @override |
| void visitMapPatternEntry(MapPatternEntry node) { |
| builder.nestExpression(); |
| visit(node.key); |
| token(node.separator); |
| soloSplit(); |
| visit(node.value); |
| builder.unnest(); |
| } |
| |
| @override |
| void visitMethodDeclaration(MethodDeclaration node) { |
| _visitFunctionOrMethodDeclaration( |
| metadata: node.metadata, |
| externalKeyword: node.externalKeyword, |
| propertyKeyword: node.propertyKeyword, |
| modifierKeyword: node.modifierKeyword, |
| operatorKeyword: node.operatorKeyword, |
| name: node.name, |
| returnType: node.returnType, |
| typeParameters: node.typeParameters, |
| formalParameters: 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. When this happens, it's important that this |
| // code here has the same rules as in [visitInstanceCreationExpression]. |
| // |
| // That ensures that the way some code is formatted is not affected by the |
| // presence or absence of `new`/`const`. In particular, it means that if |
| // they run `dart format --fix`, and then run `dart format` *again*, the |
| // second run will not produce any additional changes. |
| if (node.target == null || node.looksLikeStaticCall) { |
| // Try to keep the entire method invocation one line. |
| builder.nestExpression(); |
| builder.startSpan(); |
| |
| if (node.target != null) { |
| builder.startSpan(Cost.constructorName); |
| visit(node.target); |
| soloZeroSplit(); |
| } |
| |
| // If target is null, this will be `..` for a cascade. |
| token(node.operator); |
| visit(node.methodName); |
| |
| if (node.target != null) builder.endSpan(); |
| |
| // TODO(rnystrom): Currently, there are no constraints between a generic |
| // method's type arguments and arguments. That can lead to some funny |
| // splitting like: |
| // |
| // method<VeryLongType, |
| // AnotherTypeArgument>(argument, |
| // argument, argument, argument); |
| // |
| // The indentation is fine, but splitting in the middle of each argument |
| // list looks kind of strange. If this ends up happening in real world |
| // code, consider putting a constraint between them. |
| builder.nestExpression(); |
| visit(node.typeArguments); |
| visitArgumentList(node.argumentList); |
| builder.unnest(); |
| |
| builder.endSpan(); |
| builder.unnest(); |
| return; |
| } |
| |
| CallChainVisitor(this, node).visit(); |
| } |
| |
| @override |
| void visitMixinDeclaration(MixinDeclaration node) { |
| visitMetadata(node.metadata); |
| |
| builder.nestExpression(); |
| modifier(node.baseKeyword); |
| token(node.mixinKeyword); |
| space(); |
| token(node.name); |
| visit(node.typeParameters); |
| |
| // If there is only a single superclass constraint, format it like an |
| // "extends" in a class. |
| var onClause = node.onClause; |
| if (onClause != null && onClause.superclassConstraints.length == 1) { |
| soloSplit(); |
| token(onClause.onKeyword); |
| space(); |
| visit(onClause.superclassConstraints.single); |
| } |
| |
| builder.startRule(CombinatorRule()); |
| |
| // If there are multiple superclass constraints, format them like the |
| // "implements" clause. |
| if (onClause != null && onClause.superclassConstraints.length > 1) { |
| visit(onClause); |
| } |
| |
| visit(node.implementsClause); |
| builder.endRule(); |
| |
| space(); |
| |
| builder.unnest(); |
| _visitBody(node.leftBracket, node.members, node.rightBracket); |
| } |
| |
| @override |
| void visitNamedExpression(NamedExpression node) { |
| _visitNamedNode(node.name.label.token, node.name.colon, node.expression); |
| } |
| |
| @override |
| void visitNamedType(NamedType node) { |
| var importPrefix = node.importPrefix; |
| if (importPrefix != null) { |
| builder.startSpan(); |
| |
| token(importPrefix.name); |
| soloZeroSplit(); |
| token(importPrefix.period); |
| } |
| |
| token(node.name2); |
| visit(node.typeArguments); |
| token(node.question); |
| |
| if (importPrefix != null) { |
| builder.endSpan(); |
| } |
| } |
| |
| @override |
| void visitNativeClause(NativeClause node) { |
| token(node.nativeKeyword); |
| visit(node.name, before: space); |
| } |
| |
| @override |
| void visitNativeFunctionBody(NativeFunctionBody node) { |
| _simpleStatement(node, () { |
| builder.nestExpression(now: true); |
| soloSplit(); |
| token(node.nativeKeyword); |
| visit(node.stringLiteral, before: space); |
| builder.unnest(); |
| }); |
| } |
| |
| @override |
| void visitNullAssertPattern(NullAssertPattern node) { |
| visit(node.pattern); |
| token(node.operator); |
| } |
| |
| @override |
| void visitNullCheckPattern(NullCheckPattern node) { |
| visit(node.pattern); |
| token(node.operator); |
| } |
| |
| @override |
| void visitNullLiteral(NullLiteral node) { |
| token(node.literal); |
| } |
| |
| @override |
| void visitObjectPattern(ObjectPattern node) { |
| // Even though object patterns syntactically resemble constructor or |
| // function calls, we format them like collections (or like argument lists |
| // with trailing commas). In other words, like this: |
| // |
| // case Foo( |
| // first: 1, |
| // second: 2, |
| // third: 3 |
| // ): |
| // body; |
| // |
| // Not like: |
| // |
| // case Foo( |
| // first: 1, |
| // second: 2, |
| // third: 3): |
| // body; |
| // |
| // This is less consistent with the corresponding expression form, but is |
| // more consistent with all of the other delimited patterns -- list, map, |
| // and record -- which have collection-like formatting. |
| // TODO(rnystrom): If we move to consistently using collection-like |
| // formatting for all argument lists, then this will all be consistent and |
| // this comment should be removed. |
| visit(node.type); |
| _visitCollectionLiteral( |
| node.leftParenthesis, node.fields, node.rightParenthesis); |
| } |
| |
| @override |
| void visitOnClause(OnClause node) { |
| _visitCombinator(node.onKeyword, node.superclassConstraints); |
| } |
| |
| @override |
| void visitParenthesizedExpression(ParenthesizedExpression node) { |
| builder.nestExpression(); |
| token(node.leftParenthesis); |
| visit(node.expression); |
| builder.unnest(); |
| token(node.rightParenthesis); |
| } |
| |
| @override |
| void visitParenthesizedPattern(ParenthesizedPattern node) { |
| builder.nestExpression(); |
| token(node.leftParenthesis); |
| visit(node.pattern); |
| builder.unnest(); |
| token(node.rightParenthesis); |
| } |
| |
| @override |
| void visitPartDirective(PartDirective node) { |
| _visitDirectiveMetadata(node); |
| _simpleStatement(node, () { |
| token(node.partKeyword); |
| space(); |
| visit(node.uri); |
| }); |
| } |
| |
| @override |
| void visitPartOfDirective(PartOfDirective node) { |
| _visitDirectiveMetadata(node); |
| _simpleStatement(node, () { |
| token(node.partKeyword); |
| space(); |
| token(node.ofKeyword); |
| 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. |
| visit(node.libraryName); |
| visit(node.uri); |
| }); |
| } |
| |
| @override |
| void visitPatternAssignment(PatternAssignment node) { |
| visit(node.pattern); |
| _visitAssignment(node.equals, node.expression); |
| } |
| |
| @override |
| void visitPatternField(PatternField node) { |
| var fieldName = node.name; |
| if (fieldName != null) { |
| var name = fieldName.name; |
| if (name != null) { |
| _visitNamedNode(fieldName.name!, fieldName.colon, node.pattern); |
| } else { |
| // Named field with inferred name, like: |
| // |
| // var (:x) = (x: 1); |
| token(fieldName.colon); |
| visit(node.pattern); |
| } |
| } else { |
| visit(node.pattern); |
| } |
| } |
| |
| @override |
| void visitPatternVariableDeclaration(PatternVariableDeclaration node) { |
| visitMetadata(node.metadata); |
| builder.nestExpression(); |
| token(node.keyword); |
| space(); |
| visit(node.pattern); |
| _visitAssignment(node.equals, node.expression); |
| builder.unnest(); |
| } |
| |
| @override |
| void visitPatternVariableDeclarationStatement( |
| PatternVariableDeclarationStatement node) { |
| visit(node.declaration); |
| token(node.semicolon); |
| } |
| |
| @override |
| void visitPostfixExpression(PostfixExpression node) { |
| visit(node.operand); |
| token(node.operator); |
| } |
| |
| @override |
| void visitPrefixedIdentifier(PrefixedIdentifier node) { |
| CallChainVisitor(this, node).visit(); |
| } |
| |
| @override |
| void visitPrefixExpression(PrefixExpression node) { |
| token(node.operator); |
| |
| // Edge case: put a space after "-" if the operand is "-" or "--" so we |
| // don't merge the operators. |
| var operand = node.operand; |
| if (operand is PrefixExpression && |
| (operand.operator.lexeme == '-' || operand.operator.lexeme == '--')) { |
| space(); |
| } |
| |
| visit(node.operand); |
| } |
| |
| @override |
| void visitPropertyAccess(PropertyAccess node) { |
| if (node.isCascaded) { |
| token(node.operator); |
| visit(node.propertyName); |
| return; |
| } |
| |
| CallChainVisitor(this, node).visit(); |
| } |
| |
| @override |
| void visitRedirectingConstructorInvocation( |
| RedirectingConstructorInvocation node) { |
| builder.startSpan(); |
| |
| token(node.thisKeyword); |
| token(node.period); |
| visit(node.constructorName); |
| visit(node.argumentList); |
| |
| builder.endSpan(); |
| } |
| |
| @override |
| void visitRecordLiteral(RecordLiteral node) { |
| // Unlike other collections, try to avoid splitting record literals because |
| // doing so inside an argument list can make them hard to see. Prefer: |
| // |
| // function( |
| // (record, literal), |
| // ); |
| // |
| // Over: |
| // |
| // function(( |
| // record, |
| // literal |
| // )); |
| builder.startSpan(); |
| modifier(node.constKeyword); |
| _visitCollectionLiteral( |
| node.leftParenthesis, node.fields, node.rightParenthesis, |
| isRecord: true); |
| builder.endSpan(); |
| } |
| |
| @override |
| void visitRecordPattern(RecordPattern node) { |
| _visitCollectionLiteral( |
| node.leftParenthesis, node.fields, node.rightParenthesis, |
| isRecord: true); |
| } |
| |
| @override |
| void visitRecordTypeAnnotation(RecordTypeAnnotation node) { |
| var namedFields = node.namedFields; |
| |
| // Handle empty record types specially. |
| if (node.positionalFields.isEmpty && namedFields == null) { |
| token(node.leftParenthesis); |
| |
| // If there is a comment inside the parens, do allow splitting before it. |
| if (node.rightParenthesis.precedingComments != null) soloZeroSplit(); |
| |
| token(node.rightParenthesis); |
| token(node.question); |
| return; |
| } |
| |
| token(node.leftParenthesis); |
| builder.startRule(); |
| |
| // If all parameters are named, put the "{" right after "(". |
| if (node.positionalFields.isEmpty) { |
| token(namedFields!.leftBracket); |
| } |
| |
| // Process the parameters as a separate set of chunks. |
| builder = builder.startBlock(); |
| |
| // Write the positional fields. |
| for (var field in node.positionalFields) { |
| builder.split(nest: false, space: field != node.positionalFields.first); |
| visit(field); |
| |
| // If there is only a single positional field, the comma is mandatory. |
| var isSinglePositionalField = |
| node.positionalFields.length == 1 && namedFields == null; |
| |
| writeCommaAfter(field, |
| isTrailing: !isSinglePositionalField && |
| field == node.positionalFields.last && |
| namedFields == null); |
| } |
| |
| // Then the named fields. |
| var firstClosingDelimiter = node.rightParenthesis; |
| if (namedFields != null) { |
| if (node.positionalFields.isNotEmpty) { |
| space(); |
| token(namedFields.leftBracket); |
| } |
| |
| for (var field in namedFields.fields) { |
| builder.split(nest: false, space: field != namedFields.fields.first); |
| visit(field); |
| writeCommaAfter(field, isTrailing: field == namedFields.fields.last); |
| } |
| |
| firstClosingDelimiter = namedFields.rightBracket; |
| } |
| |
| // Put comments before the closing ")" or "}" inside the block. |
| if (firstClosingDelimiter.precedingComments != null) { |
| newline(); |
| writePrecedingCommentsAndNewlines(firstClosingDelimiter); |
| } |
| |
| builder = builder.endBlock( |
| forceSplit: _preserveTrailingCommaAfter( |
| [...node.positionalFields, ...?namedFields?.fields].last)); |
| builder.endRule(); |
| |
| // Now write the delimiter(s) themselves. |
| _writeText(firstClosingDelimiter.lexeme, firstClosingDelimiter); |
| if (namedFields != null) token(node.rightParenthesis); |
| |
| token(node.question); |
| } |
| |
| @override |
| void visitRecordTypeAnnotationNamedField( |
| RecordTypeAnnotationNamedField node) { |
| _visitParameterMetadata(node.metadata, () { |
| visit(node.type); |
| token(node.name, before: space); |
| }); |
| } |
| |
| @override |
| void visitRecordTypeAnnotationPositionalField( |
| RecordTypeAnnotationPositionalField node) { |
| _visitParameterMetadata(node.metadata, () { |
| visit(node.type); |
| token(node.name, before: space); |
| }); |
| } |
| |
| @override |
| void visitRelationalPattern(RelationalPattern node) { |
| token(node.operator); |
| space(); |
| visit(node.operand); |
| } |
| |
| @override |
| void visitRethrowExpression(RethrowExpression node) { |
| token(node.rethrowKeyword); |
| } |
| |
| @override |
| void visitRestPatternElement(RestPatternElement node) { |
| token(node.operator); |
| visit(node.pattern); |
| } |
| |
| @override |
| void visitReturnStatement(ReturnStatement node) { |
| _simpleStatement(node, () { |
| token(node.returnKeyword); |
| visit(node.expression, before: space); |
| }); |
| } |
| |
| @override |
| void visitScriptTag(ScriptTag node) { |
| // The lexeme includes the trailing newline. Strip it off since the |
| // formatter ensures it gets a newline after it. Since the script tag must |
| // come at the top of the file, we don't have to worry about preceding |
| // comments or whitespace. |
| _writeText(node.scriptTag.lexeme.trim(), node.scriptTag); |
| twoNewlines(); |
| } |
| |
| @override |
| void visitSetOrMapLiteral(SetOrMapLiteral node) { |
| _visitCollectionLiteral(node.leftBracket, node.elements, node.rightBracket, |
| constKeyword: node.constKeyword, |
| typeArguments: node.typeArguments, |
| splitOuterCollection: true); |
| } |
| |
| @override |
| void visitShowCombinator(ShowCombinator node) { |
| _visitCombinator(node.keyword, node.shownNames); |
| } |
| |
| @override |
| void visitSimpleFormalParameter(SimpleFormalParameter node) { |
| _visitParameterMetadata(node.metadata, () { |
| _beginFormalParameter(node); |
| |
| var type = node.type; |
| if (_insideNewTypedefFix && type == null) { |
| // Parameters can use "var" instead of "dynamic". Since we are inserting |
| // "dynamic" in that case, remove the "var". |
| if (node.keyword != null) { |
| if (node.keyword!.type != Keyword.VAR) { |
| modifier(node.keyword); |
| } else { |
| // Keep any comment attached to "var". |
| writePrecedingCommentsAndNewlines(node.keyword!); |
| } |
| } |
| |
| // In function declarations and the old typedef syntax, you can have a |
| // parameter name without a type. In the new syntax, you can have a type |
| // without a name. Add "dynamic" in that case. |
| |
| // Ensure comments on the identifier comes before the inserted type. |
| token(node.name, before: () { |
| _writeText('dynamic', node.name!); |
| split(); |
| }); |
| } else { |
| modifier(node.keyword); |
| visit(node.type); |
| if (node.name != null && node.type != null) split(); |
| token(node.name); |
| } |
| |
| _endFormalParameter(node); |
| }); |
| } |
| |
| @override |
| void visitSimpleIdentifier(SimpleIdentifier node) { |
| token(node.token); |
| } |
| |
| @override |
| void visitSimpleStringLiteral(SimpleStringLiteral node) { |
| _writeStringLiteral(node.literal); |
| } |
| |
| @override |
| void visitSpreadElement(SpreadElement node) { |
| token(node.spreadOperator); |
| visit(node.expression); |
| } |
| |
| @override |
| void visitStringInterpolation(StringInterpolation node) { |
| for (var element in node.elements) { |
| visit(element); |
| } |
| } |
| |
| @override |
| void visitSuperConstructorInvocation(SuperConstructorInvocation node) { |
| builder.startSpan(); |
| |
| token(node.superKeyword); |
| token(node.period); |
| visit(node.constructorName); |
| visit(node.argumentList); |
| |
| builder.endSpan(); |
| } |
| |
| @override |
| void visitSuperExpression(SuperExpression node) { |
| token(node.superKeyword); |
| } |
| |
| @override |
| void visitSuperFormalParameter(SuperFormalParameter node) { |
| _visitParameterMetadata(node.metadata, () { |
| _beginFormalParameter(node); |
| token(node.keyword, after: space); |
| visit(node.type, after: split); |
| token(node.superKeyword); |
| token(node.period); |
| token(node.name); |
| visit(node.parameters); |
| token(node.question); |
| _endFormalParameter(node); |
| }); |
| } |
| |
| @override |
| void visitSwitchExpression(SwitchExpression node) { |
| if (node.cases.isEmptyBody(node.rightBracket)) { |
| // Don't allow splitting an empty switch expression. |
| _visitSwitchValue(node.switchKeyword, node.leftParenthesis, |
| node.expression, node.rightParenthesis); |
| token(node.leftBracket); |
| token(node.rightBracket); |
| return; |
| } |
| |
| // Start the rule for splitting between the cases before the value. That |
| // way, if the value expression splits, the cases do too. Avoids: |
| // |
| // switch ([ |
| // element, |
| // ]) { inline => caseBody }; |
| builder.startRule(); |
| |
| _visitSwitchValue(node.switchKeyword, node.leftParenthesis, node.expression, |
| node.rightParenthesis); |
| |
| token(node.leftBracket); |
| builder = builder.startBlock(space: node.cases.isNotEmpty); |
| |
| if (node.cases.isNotEmpty) { |
| var first = true; |
| for (var caseNode in node.cases) { |
| if (!first) split(); |
| first = false; |
| |
| visit(caseNode); |
| |
| // The comma after the node. |
| writeCommaAfter(caseNode, isTrailing: caseNode == node.cases.last); |
| } |
| } |
| |
| _endBody(node.rightBracket, |
| forceSplit: node.cases.isNotEmpty && _preserveTrailingCommaAfter(node.cases.last)); |
| } |
| |
| @override |
| void visitSwitchExpressionCase(SwitchExpressionCase node) { |
| // If the pattern is a series of `||` patterns, then flatten them out and |
| // format them like empty cases with fallthrough in a switch statement |
| // instead of like a single indented binary pattern. Prefer: |
| // |
| // e = switch (obj) { |
| // constant1 || |
| // constant2 || |
| // constant3 => |
| // body |
| // }; |
| // |
| // Instead of: |
| // |
| // e = switch (obj) { |
| // constant1 || |
| // constant2 || |
| // constant3 => |
| // body |
| // }; |
| var orBranches = <DartPattern>[]; |
| var orTokens = <Token>[]; |
| |
| void flattenOr(DartPattern e) { |
| if (e is! LogicalOrPattern) { |
| orBranches.add(e); |
| } else { |
| flattenOr(e.leftOperand); |
| orTokens.add(e.operator); |
| flattenOr(e.rightOperand); |
| } |
| } |
| |
| flattenOr(node.guardedPattern.pattern); |
| |
| // Wrap the rule for splitting after "=>" around the pattern so that a |
| // split in the pattern forces the expression to move to the next line too. |
| builder.startLazyRule(); |
| |
| // Write the "||" operands up to the last one. |
| for (var i = 0; i < orBranches.length - 1; i++) { |
| // Note that orBranches will always have one more element than orTokens. |
| visit(orBranches[i]); |
| space(); |
| token(orTokens[i]); |
| split(); |
| } |
| |
| // Wrap the expression's nesting around the final pattern so that a split in |
| // the pattern is indented farther then the body expression. Used +2 indent |
| // because switch expressions are block-like, similar to how we split the |
| // bodies of if and for elements in collections. |
| builder.nestExpression(indent: Indent.block); |
| |
| var whenClause = node.guardedPattern.whenClause; |
| if (whenClause != null) { |
| // Wrap the when clause rule around the pattern so that if the pattern |
| // splits then we split before "when" too. |
| builder.startLazyRule(); |
| builder.nestExpression(indent: Indent.block); |
| } |
| |
| // Write the last pattern in the "||" chain. If the case pattern isn't an |
| // "||" pattern at all, this writes the one pattern. |
| visit(orBranches.last); |
| |
| if (whenClause != null) { |
| split(); |
| builder.startBlockArgumentNesting(); |
| _visitWhenClause(whenClause); |
| builder.endBlockArgumentNesting(); |
| builder.unnest(); |
| builder.endRule(); |
| } |
| |
| space(); |
| token(node.arrow); |
| split(); |
| builder.endRule(); |
| |
| builder.startBlockArgumentNesting(); |
| visit(node.expression); |
| builder.endBlockArgumentNesting(); |
| |
| builder.unnest(); |
| } |
| |
| @override |
| void visitSwitchStatement(SwitchStatement node) { |
| _visitSwitchValue(node.switchKeyword, node.leftParenthesis, node.expression, |
| node.rightParenthesis); |
| _beginBody(node.leftBracket); |
| for (var member in node.members) { |
| _visitLabels(member.labels); |
| token(member.keyword); |
| |
| if (member is SwitchCase) { |
| space(); |
| visit(member.expression); |
| } else if (member is SwitchPatternCase) { |
| space(); |
| var whenClause = member.guardedPattern.whenClause; |
| if (whenClause == null) { |
| builder.indent(); |
| visit(member.guardedPattern.pattern); |
| builder.unindent(); |
| } else { |
| // Wrap the when clause rule around the pattern so that if the pattern |
| // splits then we split before "when" too. |
| builder.startRule(); |
| builder.nestExpression(); |
| builder.startBlockArgumentNesting(); |
| visit(member.guardedPattern.pattern); |
| split(); |
| _visitWhenClause(whenClause); |
| builder.endBlockArgumentNesting(); |
| builder.unnest(); |
| builder.endRule(); |
| } |
| } else { |
| assert(member is SwitchDefault); |
| // Nothing to do. |
| } |
| |
| token(member.colon); |
| |
| if (member.statements.isNotEmpty) { |
| builder.indent(); |
| newline(); |
| visitNodes(member.statements, between: oneOrTwoNewlines); |
| builder.unindent(); |
| oneOrTwoNewlines(); |
| } else { |
| // Don't preserve blank lines between empty cases. |
| builder.writeNewline(); |
| } |
| } |
| |
| if (node.members.isNotEmpty) { |
| newline(); |
| } |
| _endBody(node.rightBracket, forceSplit: node.members.isNotEmpty); |
| } |
| |
| /// Visits the `switch (expr)` part of a switch statement or expression. |
| void _visitSwitchValue(Token switchKeyword, Token leftParenthesis, |
| Expression value, Token rightParenthesis) { |
| builder.nestExpression(); |
| token(switchKeyword); |
| space(); |
| token(leftParenthesis); |
| soloZeroSplit(); |
| visit(value); |
| token(rightParenthesis); |
| space(); |
| builder.unnest(); |
| } |
| |
| @override |
| void visitSymbolLiteral(SymbolLiteral node) { |
| token(node.poundSign); |
| var components = node.components; |
| for (var component in components) { |
| // The '.' separator |
| if (component.previous!.lexeme == '.') { |
| token(component.previous); |
| } |
| token(component); |
| } |
| } |
| |
| @override |
| void visitThisExpression(ThisExpression node) { |
| token(node.thisKeyword); |
| } |
| |
| @override |
| void visitThrowExpression(ThrowExpression node) { |
| token(node.throwKeyword); |
| space(); |
| visit(node.expression); |
| } |
| |
| @override |
| void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { |
| visitMetadata(node.metadata); |
| |
| modifier(node.externalKeyword); |
| visit(node.variables); |
| token(node.semicolon); |
| } |
| |
| @override |
| void visitTryStatement(TryStatement node) { |
| token(node.tryKeyword); |
| space(); |
| visit(node.body); |
| visitNodes(node.catchClauses, before: space, between: space); |
| token(node.finallyKeyword, before: space, after: space); |
| visit(node.finallyBlock); |
| } |
| |
| @override |
| void visitTypeArgumentList(TypeArgumentList node) { |
| _visitGenericList(node.leftBracket, node.rightBracket, node.arguments); |
| } |
| |
| @override |
| void visitTypeParameter(TypeParameter node) { |
| _visitParameterMetadata(node.metadata, () { |
| token(node.name); |
| token(node.extendsKeyword, before: space, after: space); |
| visit(node.bound); |
| }); |
| } |
| |
| @override |
| void visitTypeParameterList(TypeParameterList node) { |
| _visitGenericList(node.leftBracket, node.rightBracket, node.typeParameters); |
| } |
| |
| @override |
| void visitVariableDeclaration(VariableDeclaration node) { |
| token(node.name); |
| if (node.initializer == null) return; |
| |
| // If there are multiple variables being declared, we want to nest the |
| // initializers farther so they don't line up with the variables. Bad: |
| // |
| // var a = |
| // aValue, |
| // b = |
| // bValue; |
| // |
| // Good: |
| // |
| // var a = |
| // aValue, |
| // b = |
| // bValue; |
| var hasMultipleVariables = |
| (node.parent as VariableDeclarationList).variables.length > 1; |
| |
| _visitAssignment(node.equals!, node.initializer!, |
| nest: hasMultipleVariables); |
| } |
| |
| @override |
| void visitVariableDeclarationList(VariableDeclarationList node) { |
| visitMetadata(node.metadata); |
| |
| // Allow but try to avoid splitting between the type and name. |
| builder.startSpan(); |
| |
| modifier(node.lateKeyword); |
| modifier(node.keyword); |
| visit(node.type, after: soloSplit); |
| |
| builder.endSpan(); |
| |
| _startPossibleConstContext(node.keyword); |
| |
| // Use a single rule for all of the variables. If there are multiple |
| // declarations, we will try to keep them all on one line. If that isn't |
| // possible, we split after *every* declaration so that each is on its own |
| // line. |
| builder.startRule(); |
| |
| builder.nestExpression(); |
| |
| // If there are multiple declarations split across lines, then we want any |
| // blocks in the initializers to indent past the variables. |
| if (node.variables.length > 1) builder.startBlockArgumentNesting(); |
| |
| visitCommaSeparatedNodes(node.variables, between: split); |
| |
| if (node.variables.length > 1) builder.endBlockArgumentNesting(); |
| |
| builder.unnest(); |
| |
| builder.endRule(); |
| _endPossibleConstContext(node.keyword); |
| } |
| |
| @override |
| void visitVariableDeclarationStatement(VariableDeclarationStatement node) { |
| visit(node.variables); |
| token(node.semicolon); |
| } |
| |
| @override |
| void visitWhileStatement(WhileStatement node) { |
| builder.nestExpression(); |
| token(node.whileKeyword); |
| space(); |
| token(node.leftParenthesis); |
| soloZeroSplit(); |
| visit(node.condition); |
| token(node.rightParenthesis); |
| builder.unnest(); |
| |
| _visitLoopBody(node.body); |
| } |
| |
| @override |
| void visitWildcardPattern(WildcardPattern node) { |
| _visitVariablePattern(node.keyword, node.type, node.name); |
| } |
| |
| @override |
| void visitWithClause(WithClause node) { |
| _visitCombinator(node.withKeyword, node.mixinTypes); |
| } |
| |
| @override |
| void visitYieldStatement(YieldStatement node) { |
| _simpleStatement(node, () { |
| token(node.yieldKeyword); |
| token(node.star); |
| space(); |
| visit(node.expression); |
| }); |
| } |
| |
| /// Visit a [node], and if not null, optionally preceded or followed by the |
| /// specified functions. |
| void visit(AstNode? node, {void Function()? before, void Function()? after}) { |
| if (node == null) return; |
| |
| if (before != null) before(); |
| |
| node.accept(this); |
| |
| if (after != null) after(); |
| } |
| |
| /// Visit metadata annotations on declarations, and members. |
| /// |
| /// These always force the annotations to be on the previous line. |
| void visitMetadata(NodeList<Annotation> metadata) { |
| visitNodes(metadata, between: newline, after: newline); |
| } |
| |
| /// Visit metadata annotations for a directive. |
| /// |
| /// Always force the annotations to be on a previous line. |
| void _visitDirectiveMetadata(Directive directive) { |
| // Preserve a blank line before the first directive since users (in |
| // particular the test package) sometimes use that for metadata that |
| // applies to the entire library and not the following directive itself. |
| var isFirst = |
| directive == (directive.parent as CompilationUnit).directives.first; |
| |
| visitNodes(directive.metadata, |
| between: newline, after: isFirst ? oneOrTwoNewlines : newline); |
| } |
| |
| /// Visits metadata annotations on parameters and type parameters. |
| /// |
| /// Unlike other annotations, these are allowed to stay on the same line as |
| /// the parameter. |
| void _visitParameterMetadata( |
| NodeList<Annotation> metadata, void Function() visitParameter) { |
| if (metadata.isEmpty) { |
| visitParameter(); |
| return; |
| } |
| |
| // Split before all of the annotations on this parameter or none of them. |
| builder.startLazyRule(); |
| |
| visitNodes(metadata, between: split, after: split); |
| visitParameter(); |
| |
| // Wrap the rule around the parameter too. If it splits, we want to force |
| // the annotations to split as well. |
| builder.endRule(); |
| } |
| |
| /// Visits syntax of the form `identifier: <node>`: a named argument or a |
| /// named record field. |
| void _visitNamedNode(Token name, Token colon, AstNode node) { |
| builder.nestExpression(); |
| builder.startSpan(); |
| token(name); |
| token(colon); |
| |
| // Don't allow a split between a name and a collection. Instead, we want |
| // the collection itself to split, or to split before the argument. |
| // TODO: Write tests for named fields in patterns with collection |
| // subpatterns. |
| if (node.isDelimited) { |
| space(); |
| } else { |
| soloSplit(); |
| } |
| |
| visit(node); |
| builder.endSpan(); |
| builder.unnest(); |
| } |
| |
| Rule? _visitArgumentList(Token leftParenthesis, List<Expression> arguments, |
| Token rightParenthesis) { |
| var blockArgument = arguments.blockArgument; |
| if (blockArgument != null) { |
| _visitBlockArgumentList( |
| leftParenthesis, arguments, rightParenthesis, blockArgument); |
| return null; |
| } |
| |
| // No argument that needs special block argument handling, so format the |
| // argument list like a regular body. |
| var rule = Rule(); |
| _beginBody(leftParenthesis, rule: rule); |
| |
| for (var argument in arguments) { |
| builder.split(nest: false, space: argument != arguments.first); |
| visit(argument); |
| writeCommaAfter(argument, isTrailing: argument == arguments.last); |
| } |
| |
| _endBody(rightParenthesis, |
| forceSplit: _preserveTrailingCommaAfter(arguments.last)); |
| return rule; |
| } |
| |
| /// Visits an argument list [arguments] which contains a single argument |
| /// [blockArgument] that should have block formatting. |
| /// |
| /// We allow one collection or block function expression to have block-like |
| /// formatting, as in: |
| /// |
| /// function([ |
| /// element |
| /// ]); |
| /// |
| /// If there are multiple block-like arguments, then we don't allow any of |
| /// them to have this special formatting. This is because Flutter doesn't |
| /// seem to do that and because if that's allowed, it's not clear how to |
| /// handle the case where some of the block-like arguments need to split and |
| /// others don't. |
| void _visitBlockArgumentList( |
| Token leftParenthesis, |
| List<Expression> arguments, |
| Token rightParenthesis, |
| Expression blockArgument) { |
| var rule = ArgumentListRule(); |
| builder.startRule(rule); |
| |
| // The ")" at the end of the argument list gets no additional nesting. |
| builder.nestExpression(indent: Indent.none); |
| |
| // The arguments inside the argument list are indented depending on how the |
| // argument rule splits. |
| builder.nestExpression(rule: rule); |
| |
| builder.startBlockArgumentNesting(); |
| token(leftParenthesis); |
| |
| for (var argument in arguments) { |
| if (argument == blockArgument) { |
| // Allow the block argument to split without forcing the argument list |
| // to split. |
| rule.disableSplitOnInnerRules(); |
| } else if (argument is AdjacentStrings) { |
| // Allow adjacent strings to split without forcing the argument list |
| // to split. This is usually in functions like `test()`. |
| rule.disableSplitOnInnerRules(); |
| builder.nestExpression(); |
| } else { |
| rule.enableSplitOnInnerRules(); |
| } |
| |
| builder.split(space: argument != arguments.first); |
| visit(argument); |
| writeCommaAfter(argument, isTrailing: argument == arguments.last); |
| |
| if (argument is AdjacentStrings) builder.unnest(); |
| } |
| |
| builder.endBlockArgumentNesting(); |
| builder.unnest(); |
| |
| zeroSplit(); |
| token(rightParenthesis); |
| |
| if (_preserveTrailingCommaAfter(arguments.last)) { |
| rule.enableSplitOnInnerRules(); |
| builder.forceRules(); |
| } |
| |
| builder.unnest(); |
| builder.endRule(); |
| } |
| |
| /// Visits the `=` and the following expression in any place where an `=` |
| /// appears: |
| /// |
| /// * Assignment |
| /// * Variable declaration |
| /// * Constructor initialization |
| /// |
| /// If [nest] is true, an extra level of expression nesting is added after |
| /// the "=". |
| void _visitAssignment(Token equalsOperator, Expression rightHandSide, |
| {bool nest = false}) { |
| space(); |
| token(equalsOperator); |
| |
| if (nest) builder.nestExpression(now: true); |
| |
| // TODO: Needs tests. |
| // If the expression has a delimited body, prefer to split the body instead |
| // of at the `=`. Prefer: |
| // |
| // var x = foo( |
| // argument, |
| // ); |
| // |
| // Over: |
| // |
| // var x = |
| // foo(argument); |
| soloSplit( |
| rightHandSide.isDelimitedOrCall ? Cost.assignDelimited : Cost.assign); |
| |
| // Don't wrap the right hand side in a span. This allows initializers that |
| // are collections or function calls to split inside the body, like: |
| // |
| // variable = function( |
| // argument |
| // ); |
| // |
| // Which is what we want. It also means that other expressions won't try to |
| // adhere together, as in: |
| // |
| // variable = argument + |
| // argument; |
| // |
| // Instead of: |
| // |
| // variable = |
| // argument + argument; |
| // |
| // That's OK. We prefer that because it's consistent with the above where |
| // the style tries pretty hard to keep something on the same line as the |
| // "=". |
| visit |