blob: 6792f154fa1e03e27b239eec9fe68e2a8395a99a [file] [log] [blame] [edit]
// 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