blob: 0fbb8da9bbe02dabc3c313e75ce3ab024850c6c7 [file] [log] [blame]
// 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.
library dart_style.src.source_visitor;
import 'package:analyzer/analyzer.dart';
import 'package:analyzer/src/generated/scanner.dart';
import 'package:analyzer/src/generated/source.dart';
import 'dart_formatter.dart';
import 'chunk.dart';
import 'line_writer.dart';
import 'source_code.dart';
import 'whitespace.dart';
/// An AST visitor that drives formatting heuristics.
class SourceVisitor implements AstVisitor {
final DartFormatter _formatter;
/// The writer to which the output lines are written.
final LineWriter _writer;
/// Cached line info for calculating blank lines.
LineInfo _lineInfo;
/// The source being formatted.
final SourceCode _source;
/// `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;
/// Initialize a newly created visitor to write source code representing
/// the visited nodes to the given [writer].
SourceVisitor(formatter, this._lineInfo, SourceCode source)
: _formatter = formatter,
_source = source,
_writer = new LineWriter(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);
// Finish writing and return the complete result.
return _writer.end();
}
visitAdjacentStrings(AdjacentStrings node) {
visitNodes(node.strings, between: spaceOrNewline);
}
visitAnnotation(Annotation node) {
token(node.atSign);
visit(node.name);
token(node.period);
visit(node.constructorName);
visit(node.arguments);
}
/// 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.
visitArgumentList(ArgumentList node) {
// Don't allow any splitting in an empty argument list.
if (node.arguments.isEmpty &&
node.rightParenthesis.precedingComments == null) {
token(node.leftParenthesis);
token(node.rightParenthesis);
return;
}
// If there is just one positional argument, it tends to look weird to
// split before it, so try not to.
var singleArgument = node.arguments.length == 1 &&
node.arguments.single is ! NamedExpression;
if (singleArgument) _writer.startSpan();
// Nest around the parentheses in case there are comments before or after
// them.
_writer.nestExpression();
token(node.leftParenthesis);
// Allow splitting after "(".
var lastParam = zeroSplit();
// Try to keep the positional arguments together.
_writer.startSpan();
var i = 0;
for (; i < node.arguments.length; i++) {
var argument = node.arguments[i];
if (argument is NamedExpression) break;
visit(argument);
// Write the trailing comma and split.
if (i < node.arguments.length - 1) {
token(argument.endToken.next);
// If there are both positional and named arguments, only try to keep
// the positional ones together.
if (node.arguments[i + 1] is NamedExpression) _writer.endSpan();
// Positional arguments split independently.
lastParam = split();
}
}
// If there are named arguments, write them.
if (i < node.arguments.length) {
// Named arguments all split together, but not before the first. This
// allows all of the named arguments to get pushed to the next line, but
// stay together.
var multisplitParam = _writer.startMultisplit(separable: true);
// However, if they *do* all split, we want to split before the first one
// too. This disallows:
//
// method(first: 1,
// second: 2,
// third: 3);
multisplitParam.implies.add(lastParam);
for (; i < node.arguments.length; i++) {
var argument = node.arguments[i];
visit(argument);
// Write the trailing comma and split.
if (i < node.arguments.length - 1) {
token(argument.endToken.next);
_writer.multisplit(nest: true, space: true);
}
}
token(node.rightParenthesis);
// If there were no positional arguments, the span covers the named ones,
// so end it here.
if (node.arguments.first is NamedExpression) _writer.endSpan();
_writer.endMultisplit();
} else {
token(node.rightParenthesis);
// Keep the positional span past the ")" to include comments after the
// last argument.
_writer.endSpan();
}
if (singleArgument) _writer.endSpan();
_writer.unnest();
}
visitAsExpression(AsExpression node) {
visit(node.expression);
space();
token(node.asOperator);
space();
visit(node.type);
}
visitAssertStatement(AssertStatement node) {
_simpleStatement(node, () {
token(node.keyword);
token(node.leftParenthesis);
zeroSplit();
visit(node.condition);
token(node.rightParenthesis);
});
}
visitAssignmentExpression(AssignmentExpression node) {
visit(node.leftHandSide);
space();
token(node.operator);
split(Cost.assignment);
_writer.startSpan();
visit(node.rightHandSide);
_writer.endSpan();
}
visitAwaitExpression(AwaitExpression node) {
token(node.awaitKeyword);
space();
visit(node.expression);
}
visitBinaryExpression(BinaryExpression node) {
_writer.startMultisplit(separable: true);
_writer.startSpan();
_writer.nestExpression();
// Note that we have the full precedence table here even though some
// operators are not associative and so can never chain. In particular,
// Dart does not allow sequences of comparison or equality operators.
const operatorPrecedences = const {
// Multiplicative.
TokenType.STAR: 13,
TokenType.SLASH: 13,
TokenType.TILDE_SLASH: 13,
TokenType.PERCENT: 13,
// Additive.
TokenType.PLUS: 12,
TokenType.MINUS: 12,
// Shift.
TokenType.LT_LT: 11,
TokenType.GT_GT: 11,
// "&".
TokenType.AMPERSAND: 10,
// "^".
TokenType.CARET: 9,
// "|".
TokenType.BAR: 8,
// Relational.
TokenType.LT: 7,
TokenType.GT: 7,
TokenType.LT_EQ: 7,
TokenType.GT_EQ: 7,
// Note: as, is, and is! have the same precedence but are not handled
// like regular binary operators since they aren't associative.
// Equality.
TokenType.EQ_EQ: 6,
TokenType.BANG_EQ: 6,
// Logical and.
TokenType.AMPERSAND_AMPERSAND: 5,
// Logical or.
TokenType.BAR_BAR: 4,
};
// Flatten out a tree/chain of the same precedence. If we split on this
// precedence level, we will break all of them.
var precedence = operatorPrecedences[node.operator.type];
assert(precedence != null);
traverse(Expression e) {
if (e is BinaryExpression &&
operatorPrecedences[e.operator.type] == precedence) {
assert(operatorPrecedences[e.operator.type] != null);
traverse(e.leftOperand);
space();
token(e.operator);
_writer.multisplit(space: true, nest: true);
traverse(e.rightOperand);
} else {
visit(e);
}
}
traverse(node);
_writer.unnest();
_writer.endSpan();
_writer.endMultisplit();
}
visitBlock(Block node) {
_startBody(node.leftBracket);
visitNodes(node.statements, between: oneOrTwoNewlines, after: newline);
_endBody(node.rightBracket);
}
visitBlockFunctionBody(BlockFunctionBody node) {
// The "async" or "sync" keyword.
token(node.keyword);
// The "*" in "async*" or "sync*".
token(node.star);
if (node.keyword != null) space();
visit(node.block);
}
visitBooleanLiteral(BooleanLiteral node) {
token(node.literal);
}
visitBreakStatement(BreakStatement node) {
_simpleStatement(node, () {
token(node.keyword);
visit(node.label, before: space);
});
}
visitCascadeExpression(CascadeExpression node) {
visit(node.target);
_writer.indent();
// If there are multiple cascades, they always get their own line, even if
// they would fit.
if (node.cascadeSections.length > 1) {
newline();
visitNodes(node.cascadeSections, between: newline);
} else {
_writer.startMultisplit();
_writer.multisplit();
visitNodes(node.cascadeSections, between: _writer.multisplit);
_writer.endMultisplit();
}
_writer.unindent();
}
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);
}
visitClassDeclaration(ClassDeclaration node) {
visitDeclarationMetadata(node.metadata);
_writer.nestExpression();
modifier(node.abstractKeyword);
token(node.classKeyword);
space();
visit(node.name);
visit(node.typeParameters);
visit(node.extendsClause);
visit(node.withClause);
visit(node.implementsClause);
visit(node.nativeClause, before: space);
space();
_writer.unnest();
_startBody(node.leftBracket);
visitNodes(node.members, between: oneOrTwoNewlines, after: newline);
_endBody(node.rightBracket);
}
visitClassTypeAlias(ClassTypeAlias node) {
visitDeclarationMetadata(node.metadata);
_simpleStatement(node, () {
modifier(node.abstractKeyword);
token(node.keyword);
space();
visit(node.name);
visit(node.typeParameters);
space();
token(node.equals);
space();
visit(node.superclass);
visit(node.withClause);
visit(node.implementsClause);
});
}
visitComment(Comment node) => null;
visitCommentReference(CommentReference node) => null;
visitCompilationUnit(CompilationUnit node) {
visit(node.scriptTag);
// Put a blank line between the library tag and the other directives.
var directives = node.directives;
if (directives.isNotEmpty && directives.first is LibraryDirective) {
visit(directives.first);
twoNewlines();
directives = directives.skip(1);
}
visitNodes(directives, between: oneOrTwoNewlines);
visitNodes(node.declarations,
before: twoNewlines, between: oneOrTwoNewlines);
}
visitConditionalExpression(ConditionalExpression node) {
_writer.nestExpression();
visit(node.condition);
_writer.startSpan();
// If we split after one clause in a conditional, always split after both.
_writer.startMultisplit();
_writer.multisplit(nest: true, space: true);
token(node.question);
space();
_writer.nestExpression();
visit(node.thenExpression);
_writer.unnest();
_writer.multisplit(nest: true, space: true);
token(node.colon);
space();
visit(node.elseExpression);
_writer.endMultisplit();
_writer.endSpan();
_writer.unnest();
}
visitConstructorDeclaration(ConstructorDeclaration node) {
visitMemberMetadata(node.metadata);
modifier(node.externalKeyword);
modifier(node.constKeyword);
modifier(node.factoryKeyword);
visit(node.returnType);
token(node.period);
visit(node.name);
// Start a multisplit that spans the parameter list. We'll use this for the
// split before ":" to ensure that if the parameter list doesn't fit on one
// line that the initialization list gets pushed to its own line too.
if (node.initializers.length == 1) _writer.startMultisplit();
_visitBody(node.parameters, node.body, () {
// Check for redirects or initializer lists.
if (node.redirectedConstructor != null) {
_visitConstructorRedirects(node);
} else if (node.initializers.isNotEmpty) {
_visitConstructorInitializers(node);
}
});
}
void _visitConstructorInitializers(ConstructorDeclaration node) {
_writer.indent(2);
if (node.initializers.length == 1) {
// If there is only a single initializer, allow it on the first line, but
// only if the parameter list also fit all one line.
_writer.multisplit(space: true);
_writer.endMultisplit();
} else {
newline();
}
token(node.separator); // ":".
space();
for (var i = 0; i < node.initializers.length; i++) {
if (i > 0) {
// Preceding comma.
token(node.initializers[i].beginToken.previous);
// Indent subsequent fields one more so they line up with the first
// field following the ":":
//
// Foo()
// : first,
// second;
if (i == 1) _writer.indent();
newline();
}
node.initializers[i].accept(this);
}
// If there were multiple fields, discard their extra indentation.
if (node.initializers.length > 1) _writer.unindent();
_writer.unindent(2);
}
void _visitConstructorRedirects(ConstructorDeclaration node) {
token(node.separator /* = */, before: space, after: space);
visitCommaSeparatedNodes(node.initializers);
visit(node.redirectedConstructor);
}
visitConstructorFieldInitializer(ConstructorFieldInitializer node) {
token(node.keyword);
token(node.period);
visit(node.fieldName);
space();
token(node.equals);
space();
visit(node.expression);
}
visitConstructorName(ConstructorName node) {
visit(node.type);
token(node.period);
visit(node.name);
}
visitContinueStatement(ContinueStatement node) {
_simpleStatement(node, () {
token(node.keyword);
visit(node.label, before: space);
});
}
visitDeclaredIdentifier(DeclaredIdentifier node) {
modifier(node.keyword);
visit(node.type, after: space);
visit(node.identifier);
}
visitDefaultFormalParameter(DefaultFormalParameter node) {
visit(node.parameter);
if (node.separator != null) {
// The '=' separator is preceded by a space.
if (node.separator.type == TokenType.EQ) {
space();
}
token(node.separator);
visit(node.defaultValue, before: space);
}
}
visitDoStatement(DoStatement node) {
_simpleStatement(node, () {
token(node.doKeyword);
space();
visit(node.body);
space();
token(node.whileKeyword);
space();
token(node.leftParenthesis);
zeroSplit();
visit(node.condition);
token(node.rightParenthesis);
});
}
visitDoubleLiteral(DoubleLiteral node) {
token(node.literal);
}
visitEmptyFunctionBody(EmptyFunctionBody node) {
token(node.semicolon);
}
visitEmptyStatement(EmptyStatement node) {
token(node.semicolon);
}
visitEnumConstantDeclaration(EnumConstantDeclaration node) {
visit(node.name);
}
visitEnumDeclaration(EnumDeclaration node) {
visitDeclarationMetadata(node.metadata);
token(node.keyword);
space();
visit(node.name);
space();
_startBody(node.leftBracket, space: true);
visitCommaSeparatedNodes(node.constants, between: () {
_writer.multisplit(space: true);
});
_endBody(node.rightBracket, space: true);
}
visitExportDirective(ExportDirective node) {
visitDeclarationMetadata(node.metadata);
_simpleStatement(node, () {
token(node.keyword);
space();
visit(node.uri);
_visitCombinators(node.combinators);
});
}
visitExpressionFunctionBody(ExpressionFunctionBody node) {
_simpleStatement(node, () {
// The "async" or "sync" keyword.
token(node.keyword, after: space);
// Try to keep the "(...) => " with the start of the body for anonymous
// functions.
if (_isLambda(node)) _writer.startSpan();
token(node.functionDefinition); // "=>".
split();
if (_isLambda(node)) _writer.endSpan();
_writer.startSpan();
visit(node.expression);
_writer.endSpan();
});
}
visitExpressionStatement(ExpressionStatement node) {
_simpleStatement(node, () {
visit(node.expression);
});
}
visitExtendsClause(ExtendsClause node) {
split();
token(node.keyword);
space();
visit(node.superclass);
}
visitFieldDeclaration(FieldDeclaration node) {
visitMemberMetadata(node.metadata);
_simpleStatement(node, () {
modifier(node.staticKeyword);
visit(node.fields);
});
}
visitFieldFormalParameter(FieldFormalParameter node) {
token(node.keyword, after: space);
visit(node.type, after: space);
token(node.thisToken);
token(node.period);
visit(node.identifier);
visit(node.parameters);
}
visitForEachStatement(ForEachStatement node) {
_writer.nestExpression();
token(node.awaitKeyword, after: space);
token(node.forKeyword);
space();
token(node.leftParenthesis);
if (node.loopVariable != null) {
visit(node.loopVariable);
} else {
visit(node.identifier);
}
split();
token(node.inKeyword);
space();
visit(node.iterable);
token(node.rightParenthesis);
space();
visit(node.body);
_writer.unnest();
}
visitFormalParameterList(FormalParameterList node) {
_writer.nestExpression();
token(node.leftParenthesis);
// Allow splitting after the "(" in non-empty parameter lists, but not for
// lambdas.
if ((node.parameters.isNotEmpty ||
node.rightParenthesis.precedingComments != null) &&
!_isLambda(node)) {
zeroSplit();
}
// Try to keep the parameters together.
_writer.startSpan();
var inOptionalParams = false;
for (var i = 0; i < node.parameters.length; i++) {
var parameter = node.parameters[i];
var inFirstOptional =
!inOptionalParams && parameter is DefaultFormalParameter;
// Preceding comma.
if (i > 0) token(node.parameters[i - 1].endToken.next);
// Don't try to keep optional parameters together with mandatory ones.
if (inFirstOptional) _writer.endSpan();
if (i > 0) split();
if (inFirstOptional) {
// Do try to keep optional parameters with each other.
_writer.startSpan();
// "[" or "{" for optional parameters.
token(node.leftDelimiter);
inOptionalParams = true;
}
visit(parameter);
}
// "]" or "}" for optional parameters.
token(node.rightDelimiter);
token(node.rightParenthesis);
_writer.unnest();
_writer.endSpan();
}
visitForStatement(ForStatement node) {
_writer.nestExpression();
token(node.forKeyword);
space();
token(node.leftParenthesis);
_writer.startMultisplit();
// The initialization clause.
if (node.initialization != null) {
visit(node.initialization);
} else if (node.variables != null) {
// Indent split variables more so they aren't at the same level
// as the rest of the loop clauses.
_writer.indent(4);
var declaration = node.variables;
visitDeclarationMetadata(declaration.metadata);
modifier(declaration.keyword);
visit(declaration.type, after: space);
visitCommaSeparatedNodes(declaration.variables, between: () {
_writer.multisplit(space: true, nest: true);
});
_writer.unindent(4);
}
token(node.leftSeparator);
// The condition clause.
if (node.condition != null) _writer.multisplit(nest: true, space: true);
visit(node.condition);
token(node.rightSeparator);
// The update clause.
if (node.updaters.isNotEmpty) {
_writer.multisplit(nest: true, space: true);
visitCommaSeparatedNodes(node.updaters,
between: () => _writer.multisplit(nest: true, space: true));
}
token(node.rightParenthesis);
_writer.endMultisplit();
_writer.unnest();
// The body.
if (node.body is! EmptyStatement) space();
visit(node.body);
}
visitFunctionDeclaration(FunctionDeclaration node) {
visitMemberMetadata(node.metadata);
_writer.nestExpression();
modifier(node.externalKeyword);
visit(node.returnType, after: space);
modifier(node.propertyKeyword);
visit(node.name);
visit(node.functionExpression);
_writer.unnest();
}
visitFunctionDeclarationStatement(FunctionDeclarationStatement node) {
visit(node.functionDeclaration);
}
visitFunctionExpression(FunctionExpression node) {
_visitBody(node.parameters, node.body);
}
visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
visit(node.function);
visit(node.argumentList);
}
visitFunctionTypeAlias(FunctionTypeAlias node) {
visitDeclarationMetadata(node.metadata);
_simpleStatement(node, () {
token(node.keyword);
space();
visit(node.returnType, after: space);
visit(node.name);
visit(node.typeParameters);
visit(node.parameters);
});
}
visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) {
visit(node.returnType, after: space);
// Try to keep the function's parameters with its name.
_writer.startSpan();
visit(node.identifier);
visit(node.parameters);
_writer.endSpan();
}
visitHideCombinator(HideCombinator node) {
_visitCombinator(node.keyword, node.hiddenNames);
}
visitIfStatement(IfStatement node) {
_writer.nestExpression();
token(node.ifKeyword);
space();
token(node.leftParenthesis);
visit(node.condition);
token(node.rightParenthesis);
space();
visit(node.thenStatement);
_writer.unnest();
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);
space();
visit(node.elseStatement);
}
}
visitImplementsClause(ImplementsClause node) {
split();
token(node.keyword);
space();
visitCommaSeparatedNodes(node.interfaces);
}
visitImportDirective(ImportDirective node) {
visitDeclarationMetadata(node.metadata);
_simpleStatement(node, () {
token(node.keyword);
space();
visit(node.uri);
token(node.deferredToken, before: space);
token(node.asToken, before: split, after: space);
visit(node.prefix);
_visitCombinators(node.combinators);
});
}
visitIndexExpression(IndexExpression node) {
if (node.isCascaded) {
token(node.period);
} else {
visit(node.target);
}
_writer.startSpan();
token(node.leftBracket);
_writer.nestExpression();
zeroSplit();
visit(node.index);
token(node.rightBracket);
_writer.unnest();
_writer.endSpan();
}
visitInstanceCreationExpression(InstanceCreationExpression node) {
_writer.startSpan();
token(node.keyword);
space();
visit(node.constructorName);
visit(node.argumentList);
_writer.endSpan();
}
visitIntegerLiteral(IntegerLiteral node) {
token(node.literal);
}
visitInterpolationExpression(InterpolationExpression node) {
token(node.leftBracket);
visit(node.expression);
token(node.rightBracket);
}
visitInterpolationString(InterpolationString node) {
token(node.contents);
}
visitIsExpression(IsExpression node) {
visit(node.expression);
space();
token(node.isOperator);
token(node.notOperator);
space();
visit(node.type);
}
visitLabel(Label node) {
visit(node.label);
token(node.colon);
}
visitLabeledStatement(LabeledStatement node) {
visitNodes(node.labels, between: space, after: space);
visit(node.statement);
}
visitLibraryDirective(LibraryDirective node) {
visitDeclarationMetadata(node.metadata);
_simpleStatement(node, () {
token(node.keyword);
space();
visit(node.name);
});
}
visitLibraryIdentifier(LibraryIdentifier node) {
visit(node.components.first);
for (var component in node.components.skip(1)) {
token(component.beginToken.previous); // "."
visit(component);
}
}
visitListLiteral(ListLiteral node) {
_visitCollectionLiteral(
node, node.leftBracket, node.elements, node.rightBracket);
}
visitMapLiteral(MapLiteral node) {
_visitCollectionLiteral(
node, node.leftBracket, node.entries, node.rightBracket);
}
visitMapLiteralEntry(MapLiteralEntry node) {
visit(node.key);
token(node.separator);
split();
visit(node.value);
}
visitMethodDeclaration(MethodDeclaration node) {
visitMemberMetadata(node.metadata);
modifier(node.externalKeyword);
modifier(node.modifierKeyword);
visit(node.returnType, after: space);
modifier(node.propertyKeyword);
modifier(node.operatorKeyword);
visit(node.name);
_visitBody(node.parameters, node.body);
}
visitMethodInvocation(MethodInvocation node) {
// With a chain of method calls like `foo.bar.baz.bang`, they either all
// split or none of them do.
var startedMultisplit = false;
// Try to keep the entire method chain one line.
_writer.startSpan();
_writer.nestExpression();
// Recursively walk the chain of method calls.
var depth = 0;
visitInvocation(invocation) {
depth++;
var hasTarget = true;
if (invocation.target is MethodInvocation) {
visitInvocation(invocation.target);
} else if (invocation.period != null) {
visit(invocation.target);
} else {
hasTarget = false;
}
if (hasTarget) {
// Don't start the multisplit until after the first target. This
// ensures we don't get tripped up by newlines or comments before the
// first target.
if (!startedMultisplit) {
_writer.startMultisplit(separable: true);
startedMultisplit = true;
}
_writer.multisplit(nest: true);
token(invocation.period);
}
visit(invocation.methodName);
// Stop the multisplit after the last call, but before it's arguments.
// That allows unsplit chains where the last argument list wraps, like:
//
// foo().bar().baz(
// argument, list);
depth--;
if (depth == 0 && startedMultisplit) _writer.endMultisplit();
visit(invocation.argumentList);
}
visitInvocation(node);
_writer.unnest();
_writer.endSpan();
}
visitNamedExpression(NamedExpression node) {
visit(node.name);
visit(node.expression, before: space);
}
visitNativeClause(NativeClause node) {
token(node.keyword);
space();
visit(node.name);
}
visitNativeFunctionBody(NativeFunctionBody node) {
_simpleStatement(node, () {
token(node.nativeToken);
space();
visit(node.stringLiteral);
});
}
visitNullLiteral(NullLiteral node) {
token(node.literal);
}
visitParenthesizedExpression(ParenthesizedExpression node) {
_writer.nestExpression();
token(node.leftParenthesis);
visit(node.expression);
_writer.unnest();
token(node.rightParenthesis);
}
visitPartDirective(PartDirective node) {
_simpleStatement(node, () {
token(node.keyword);
space();
visit(node.uri);
});
}
visitPartOfDirective(PartOfDirective node) {
_simpleStatement(node, () {
token(node.keyword);
space();
token(node.ofToken);
space();
visit(node.libraryName);
});
}
visitPostfixExpression(PostfixExpression node) {
visit(node.operand);
token(node.operator);
}
visitPrefixedIdentifier(PrefixedIdentifier node) {
visit(node.prefix);
token(node.period);
visit(node.identifier);
}
visitPrefixExpression(PrefixExpression node) {
token(node.operator);
// Corner case: put a space between successive "-" operators so we don't
// inadvertently turn them into a "--" decrement operator.
if (node.operand is PrefixExpression &&
node.operand.operator.lexeme == "-") {
space();
}
visit(node.operand);
}
visitPropertyAccess(PropertyAccess node) {
if (node.isCascaded) {
token(node.operator);
} else {
visit(node.target);
token(node.operator);
}
visit(node.propertyName);
}
visitRedirectingConstructorInvocation(RedirectingConstructorInvocation node) {
_writer.startSpan();
token(node.keyword);
token(node.period);
visit(node.constructorName);
visit(node.argumentList);
_writer.endSpan();
}
visitRethrowExpression(RethrowExpression node) {
token(node.keyword);
}
visitReturnStatement(ReturnStatement node) {
_simpleStatement(node, () {
token(node.keyword);
if (node.expression != null) {
space();
visit(node.expression);
}
});
}
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.offset);
oneOrTwoNewlines();
}
visitShowCombinator(ShowCombinator node) {
_visitCombinator(node.keyword, node.shownNames);
}
visitSimpleFormalParameter(SimpleFormalParameter node) {
visitParameterMetadata(node.metadata);
modifier(node.keyword);
visit(node.type, after: space);
visit(node.identifier);
}
visitSimpleIdentifier(SimpleIdentifier node) {
token(node.token);
}
visitSimpleStringLiteral(SimpleStringLiteral node) {
// Since we output the string literal manually, ensure any preceding
// comments are written first.
writePrecedingCommentsAndNewlines(node.literal);
_writeStringLiteral(node.literal.lexeme, node.offset);
}
visitStringInterpolation(StringInterpolation node) {
// Since we output the interpolated text manually, ensure we include any
// preceding stuff first.
writePrecedingCommentsAndNewlines(node.beginToken);
// Right now, the formatter does not try to do any reformatting of the
// contents of interpolated strings. Instead, it treats the entire thing as
// a single (possibly multi-line) chunk of text.
_writeStringLiteral(
_source.text.substring(node.beginToken.offset, node.endToken.end),
node.offset);
}
visitSuperConstructorInvocation(SuperConstructorInvocation node) {
_writer.startSpan();
token(node.keyword);
token(node.period);
visit(node.constructorName);
visit(node.argumentList);
_writer.endSpan();
}
visitSuperExpression(SuperExpression node) {
token(node.keyword);
}
visitSwitchCase(SwitchCase node) {
visitNodes(node.labels, between: space, after: space);
token(node.keyword);
space();
visit(node.expression);
token(node.colon);
_writer.indent();
// TODO(rnystrom): Allow inline cases?
newline();
visitNodes(node.statements, between: oneOrTwoNewlines);
_writer.unindent();
}
visitSwitchDefault(SwitchDefault node) {
visitNodes(node.labels, between: space, after: space);
token(node.keyword);
token(node.colon);
_writer.indent();
// TODO(rnystrom): Allow inline cases?
newline();
visitNodes(node.statements, between: oneOrTwoNewlines);
_writer.unindent();
}
visitSwitchStatement(SwitchStatement node) {
_writer.nestExpression();
token(node.keyword);
space();
token(node.leftParenthesis);
zeroSplit();
visit(node.expression);
token(node.rightParenthesis);
space();
token(node.leftBracket);
_writer.indent();
newline();
visitNodes(node.members, between: oneOrTwoNewlines, after: newline);
token(node.rightBracket, before: () {
_writer.unindent();
newline();
});
_writer.unnest();
}
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);
}
}
visitThisExpression(ThisExpression node) {
token(node.keyword);
}
visitThrowExpression(ThrowExpression node) {
token(node.keyword);
space();
visit(node.expression);
}
visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
visitDeclarationMetadata(node.metadata);
_simpleStatement(node, () {
visit(node.variables);
});
}
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);
}
visitTypeArgumentList(TypeArgumentList node) {
token(node.leftBracket);
visitCommaSeparatedNodes(node.arguments);
token(node.rightBracket);
}
visitTypeName(TypeName node) {
visit(node.name);
visit(node.typeArguments);
}
visitTypeParameter(TypeParameter node) {
visitParameterMetadata(node.metadata);
visit(node.name);
token(node.keyword /* extends */, before: space, after: space);
visit(node.bound);
}
visitTypeParameterList(TypeParameterList node) {
token(node.leftBracket);
visitCommaSeparatedNodes(node.typeParameters);
token(node.rightBracket);
}
visitVariableDeclaration(VariableDeclaration node) {
visit(node.name);
if (node.initializer == null) return;
space();
token(node.equals);
split(Cost.assignment);
_writer.startSpan();
visit(node.initializer);
_writer.endSpan();
}
visitVariableDeclarationList(VariableDeclarationList node) {
visitDeclarationMetadata(node.metadata);
modifier(node.keyword);
visit(node.type, after: space);
if (node.variables.length == 1) {
visit(node.variables.single);
return;
}
// If there are multiple declarations and any of them have initializers,
// put them all on their own lines.
if (node.variables.any((variable) => variable.initializer != null)) {
visit(node.variables.first);
// Indent variables after the first one to line up past "var".
_writer.indent(2);
for (var variable in node.variables.skip(1)) {
token(variable.beginToken.previous); // Comma.
newline();
visit(variable);
}
_writer.unindent(2);
return;
}
// Use a multisplit between 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.
_writer.startMultisplit();
visitCommaSeparatedNodes(node.variables, between: () {
_writer.multisplit(space: true, nest: true);
});
_writer.endMultisplit();
}
visitVariableDeclarationStatement(VariableDeclarationStatement node) {
_simpleStatement(node, () {
visit(node.variables);
});
}
visitWhileStatement(WhileStatement node) {
_writer.nestExpression();
token(node.keyword);
space();
token(node.leftParenthesis);
zeroSplit();
visit(node.condition);
token(node.rightParenthesis);
if (node.body is! EmptyStatement) space();
visit(node.body);
_writer.unnest();
}
visitWithClause(WithClause node) {
split();
token(node.withKeyword);
space();
visitCommaSeparatedNodes(node.mixinTypes);
}
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 before(), void after()}) {
if (node == null) return;
if (before != null) before();
node.accept(this);
if (after != null) after();
}
/// Visit metadata annotations on directives and declarations.
///
/// These always force the annotations to be on the previous line.
void visitDeclarationMetadata(NodeList<Annotation> metadata) {
// If there are multiple annotations, they are always on their own lines,
// even the last.
if (metadata.length > 1) {
visitNodes(metadata, between: newline, after: newline);
} else {
visitNodes(metadata, between: space, after: newline);
}
}
/// Visit metadata annotations on members.
///
/// These may be on the same line as the member, or on the previous.
void visitMemberMetadata(NodeList<Annotation> metadata) {
// If there are multiple annotations, they are always on their own lines,
// even the last.
if (metadata.length > 1) {
visitNodes(metadata, between: newline, after: newline);
} else {
visitNodes(metadata, between: space, after: spaceOrNewline);
}
}
/// Visit metadata annotations on parameters and type parameters.
///
/// These are always on the same line as the parameter.
void visitParameterMetadata(NodeList<Annotation> metadata) {
// TODO(rnystrom): Allow splitting after annotations?
visitNodes(metadata, between: space, after: space);
}
/// Visit the given function [parameters] followed by its [body], printing a
/// space before it if it's not empty.
///
/// If [afterParameters] is provided, it is invoked between the parameters
/// and body. (It's used for constructor initialization lists.)
void _visitBody(FormalParameterList parameters, FunctionBody body,
[afterParameters()]) {
if (parameters != null) {
// If the body is "=>", add an extra level of indentation around the
// parameters. This ensures that if they wrap, they wrap more deeply than
// the "=>" does, as in:
//
// someFunction(parameter,
// parameter, parameter) =>
// "the body";
if (body is ExpressionFunctionBody) _writer.nestExpression();
visit(parameters);
if (afterParameters != null) afterParameters();
if (body is ExpressionFunctionBody) _writer.unnest();
}
if (body is! EmptyFunctionBody) space();
visit(body);
}
/// Visit a list of [nodes] if not null, optionally separated and/or preceded
/// and followed by the given functions.
void visitNodes(Iterable<AstNode> nodes, {before(), between(), after()}) {
if (nodes == null || nodes.isEmpty) return;
if (before != null) before();
visit(nodes.first);
for (var node in nodes.skip(1)) {
if (between != null) between();
visit(node);
}
if (after != null) after();
}
/// Visit a comma-separated list of [nodes] if not null.
void visitCommaSeparatedNodes(Iterable<AstNode> nodes, {between()}) {
if (nodes == null || nodes.isEmpty) return;
if (between == null) between = space;
var first = true;
for (var node in nodes) {
if (!first) between();
first = false;
visit(node);
// The comma after the node.
if (node.endToken.next.lexeme == ",") token(node.endToken.next);
}
}
/// Visits the collection literal [node] whose body starts with [leftBracket],
/// ends with [rightBracket] and contains [elements].
void _visitCollectionLiteral(TypedLiteral node, Token leftBracket,
Iterable<AstNode> elements, Token rightBracket) {
modifier(node.constKeyword);
visit(node.typeArguments);
_startBody(leftBracket);
// Each list element takes at least 3 characters (one character for the
// element, one for the comma, one for the space), so force it to split if
// we know that won't fit.
if (elements.length > _writer.pageWidth ~/ 3) _writer.preemptMultisplits();
for (var element in elements) {
if (element != elements.first) _writer.multisplit(space: true);
_writer.nestExpression();
visit(element);
// The comma after the element.
if (element.endToken.next.lexeme == ",") token(element.endToken.next);
_writer.unnest();
}
_endBody(rightBracket);
}
/// Visits a list of [combinators] following an "import" or "export"
/// directive. Combinators can be split in a few different ways:
///
/// // All on one line:
/// import 'animals.dart' show Ant hide Cat;
///
/// // Wrap before each keyword:
/// import 'animals.dart'
/// show Ant, Baboon
/// hide Cat;
///
/// // Wrap either or both of the name lists:
/// import 'animals.dart'
/// show
/// Ant,
/// Baboon
/// hide Cat;
///
/// Multisplits are used here to specifically avoid a few undesirable
/// combinations:
///
/// // Wrap list but not keyword:
/// import 'animals.dart' show
/// Ant,
/// Baboon
/// hide Cat;
///
/// // Wrap one keyword but not both:
/// import 'animals.dart'
/// show Ant, Baboon hide Cat;
///
/// This ensures that when any wrapping occurs, the keywords are always at
/// the beginning of the line.
void _visitCombinators(NodeList<Combinator> combinators) {
if (combinators.isEmpty) return;
_writer.startMultisplit();
visitNodes(combinators);
_writer.endMultisplit();
}
/// Visits a [HideCombinator] or [ShowCombinator] starting with [keyword] and
/// containing [names].
///
/// This assumes it has been called from within the [Multisplit] created by
/// [_visitCombinators].
void _visitCombinator(Token keyword, NodeList<SimpleIdentifier> names) {
// Allow splitting after the keyword.
_writer.multisplit(space: true, nest: true);
_writer.nestExpression();
token(keyword);
_writer.startMultisplit();
_writer.multisplit(nest: true, space: true);
visitCommaSeparatedNodes(names,
between: () => _writer.multisplit(nest: true, space: true));
_writer.unnest();
_writer.endMultisplit();
}
/// Writes the simple statement or semicolon-delimited top-level declaration.
///
/// Handles nesting if a line break occurs in the statement and writes the
/// terminating semicolon. Invokes [body] which should write statement itself.
void _simpleStatement(AstNode node, body()) {
_writer.nestExpression();
body();
// TODO(rnystrom): Can the analyzer move "semicolon" to some shared base
// type?
token((node as dynamic).semicolon);
_writer.unnest();
}
/// Writes an opening bracket token ("(", "{", "[") and handles indenting and
/// starting the multisplit it contains.
///
/// If [space] is `true`, then the initial multisplit will use a space if not
/// split.
void _startBody(Token leftBracket, {bool space: false}) {
token(leftBracket);
// Indent the body.
_writer.startMultisplit();
_writer.indent();
// Split after the bracket.
_writer.multisplit(space: space);
}
/// Writes a closing bracket token (")", "}", "]") and handles unindenting
/// and ending the multisplit it contains.
///
/// Used for blocks, other curly bodies, and collection literals.
///
/// If [space] is `true`, then the initial multisplit will use a space if not
/// split.
void _endBody(Token rightBracket, {bool space: false}) {
token(rightBracket, before: () {
// Split before the closing bracket character.
_writer.unindent();
_writer.multisplit(space: space);
});
_writer.endMultisplit();
}
/// Returns `true` if [node] is immediately contained within an anonymous
/// [FunctionExpression].
bool _isLambda(AstNode node) =>
node.parent is FunctionExpression &&
node.parent.parent is! FunctionDeclaration;
/// Writes the string literal [string] to the output.
///
/// Splits multiline strings into separate chunks so that the line splitter
/// can handle them correctly.
void _writeStringLiteral(String string, int offset) {
// Split each line of a multiline string into separate chunks.
var lines = string.split(_formatter.lineEnding);
_writeText(lines.first, offset);
offset += lines.first.length;
for (var line in lines.skip(1)) {
_writer.writeWhitespace(Whitespace.newlineFlushLeft);
offset++;
_writeText(line, offset);
offset += line.length;
}
}
/// Emit the given [modifier] if it's non null, followed by non-breaking
/// whitespace.
void modifier(Token modifier) {
token(modifier, after: space);
}
/// Emit a non-breaking space.
void space() {
_writer.writeWhitespace(Whitespace.space);
}
/// Emit a single mandatory newline.
void newline() {
_writer.writeWhitespace(Whitespace.newline);
}
/// Emit a two mandatory newlines.
void twoNewlines() {
_writer.writeWhitespace(Whitespace.twoNewlines);
}
/// Allow either a single space or newline to be emitted before the next
/// non-whitespace token based on whether a newline exists in the source
/// between the last token and the next one.
void spaceOrNewline() {
_writer.writeWhitespace(Whitespace.spaceOrNewline);
}
/// Allow either one or two newlines to be emitted before the next
/// non-whitespace token based on whether more than one newline exists in the
/// source between the last token and the next one.
void oneOrTwoNewlines() {
_writer.writeWhitespace(Whitespace.oneOrTwoNewlines);
}
/// Writes a single-space split with the given [cost].
///
/// If [cost] is omitted, defaults to [Cost.normal]. Returns the newly created
/// [SplitParam].
SplitParam split([int cost]) => _writer.writeSplit(cost: cost, space: true);
/// Writes a split that is the empty string when unsplit.
///
/// Returns the newly created [SplitParam].
SplitParam zeroSplit() => _writer.writeSplit();
/// Emit [token], along with any comments and formatted whitespace that comes
/// before it.
///
/// Does nothing if [token] is `null`. If [before] is given, it will be
/// executed before the token is outout. Likewise, [after] will be called
/// after the token is output.
void token(Token token, {before(), after()}) {
if (token == null) return;
writePrecedingCommentsAndNewlines(token);
if (before != null) before();
_writeText(token.lexeme, token.offset);
if (after != null) after();
}
/// Writes all formatted whitespace and comments that appear before [token].
void writePrecedingCommentsAndNewlines(Token token) {
var comment = token.precedingComments;
// For performance, avoid calculating newlines between tokens unless
// actually needed.
if (comment == null) {
if (_writer.needsToPreserveNewlines) {
_writer.preserveNewlines(_startLine(token) - _endLine(token.previous));
}
return;
}
var previousLine = _endLine(token.previous);
// Corner case! The analyzer includes the "\n" in the script tag's lexeme,
// which means it appears to be one line later than it is. That causes a
// comment following it to appear to be on the same line. Fix that here by
// correcting the script tag's line.
if (token.previous.type == TokenType.SCRIPT_TAG) previousLine--;
var tokenLine = _startLine(token);
var comments = [];
while (comment != null) {
var commentLine = _startLine(comment);
// Don't preserve newlines at the top of the file.
if (comment == token.precedingComments &&
token.previous.type == TokenType.EOF) {
previousLine = commentLine;
}
var sourceComment = new SourceComment(comment.toString().trim(),
commentLine - previousLine,
isLineComment: comment.type == TokenType.SINGLE_LINE_COMMENT,
isStartOfLine: _startColumn(comment) == 1);
// If this comment contains either of the selection endpoints, mark them
// in the comment.
var start = _getSelectionStartWithin(comment.offset, comment.length);
if (start != null) sourceComment.startSelection(start);
var end = _getSelectionEndWithin(comment.offset, comment.length);
if (end != null) sourceComment.endSelection(end);
comments.add(sourceComment);
previousLine = _endLine(comment);
comment = comment.next;
}
_writer.writeComments(comments, tokenLine - previousLine, token.lexeme);
}
/// Write [text] to the current chunk, given that it starts at [offset] in
/// the original source.
///
/// Also outputs the selection endpoints if needed.
void _writeText(String text, int offset) {
_writer.write(text);
// If this text contains either of the selection endpoints, mark them in
// the chunk.
var start = _getSelectionStartWithin(offset, text.length);
if (start != null) {
_writer.startSelectionFromEnd(text.length - start);
}
var end = _getSelectionEndWithin(offset, text.length);
if (end != null) {
_writer.endSelectionFromEnd(text.length - end);
}
}
/// Returns the number of characters past [offset] in the source where the
/// selection start appears if it appears before `offset + length`.
///
/// Returns `null` if the selection start has already been processed or is
/// not within that range.
int _getSelectionStartWithin(int offset, int length) {
// If there is no selection, do nothing.
if (_source.selectionStart == null) return null;
// If we've already passed it, don't consider it again.
if (_passedSelectionStart) return null;
var start = _source.selectionStart - offset;
// If it started in whitespace before this text, push it forward to the
// beginning of the non-whitespace text.
if (start < 0) start = 0;
// If we haven't reached it yet, don't consider it.
if (start >= length) return null;
// We found it.
_passedSelectionStart = true;
return start;
}
/// Returns the number of characters past [offset] in the source where the
/// selection endpoint appears if it appears before `offset + length`.
///
/// Returns `null` if the selection endpoint has already been processed or is
/// not within that range.
int _getSelectionEndWithin(int offset, int length) {
// If there is no selection, do nothing.
if (_source.selectionLength == null) return null;
// If we've already passed it, don't consider it again.
if (_passedSelectionEnd) return null;
var end = _findSelectionEnd() - offset;
// If it started in whitespace before this text, push it forward to the
// beginning of the non-whitespace text.
if (end < 0) end = 0;
// If we haven't reached it yet, don't consider it.
if (end > length) return null;
if (end == length && _findSelectionEnd() == _source.selectionStart) {
return null;
}
// We found it.
_passedSelectionEnd = true;
return end;
}
/// Calculates the character offset in the source text of the end of the
/// selection.
///
/// Removes any trailing whitespace from the selection.
int _findSelectionEnd() {
if (_selectionEnd != null) return _selectionEnd;
_selectionEnd = _source.selectionStart + _source.selectionLength;
// If the selection bumps to the end of the source, pin it there.
if (_selectionEnd == _source.text.length) return _selectionEnd;
// Trim off any trailing whitespace. We want the selection to "rubberband"
// around the selected non-whitespace tokens since the whitespace will
// be munged by the formatter itself.
while (_selectionEnd > _source.selectionStart) {
// Stop if we hit anything other than space, tab, newline or carriage
// return.
var char = _source.text.codeUnitAt(_selectionEnd - 1);
if (char != 0x20 && char != 0x09 && char != 0x0a && char != 0x0d) {
break;
}
_selectionEnd--;
}
return _selectionEnd;
}
/// Gets the 1-based line number that the beginning of [token] lies on.
int _startLine(Token token) => _lineInfo.getLocation(token.offset).lineNumber;
/// Gets the 1-based line number that the end of [token] lies on.
int _endLine(Token token) => _lineInfo.getLocation(token.end).lineNumber;
/// Gets the 1-based column number that the beginning of [token] lies on.
int _startColumn(Token token) =>
_lineInfo.getLocation(token.offset).columnNumber;
}