blob: e13598a0a6ab09ca16271f8fe6ae86dcb9b070b3 [file] [log] [blame]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/source/line_info.dart';
import '../ast_extensions.dart';
import '../constants.dart';
import '../dart_formatter.dart';
import '../piece/adjacent.dart';
import '../piece/adjacent_strings.dart';
import '../piece/assign.dart';
import '../piece/constructor.dart';
import '../piece/for.dart';
import '../piece/if.dart';
import '../piece/infix.dart';
import '../piece/list.dart';
import '../piece/piece.dart';
import '../piece/variable.dart';
import '../source_code.dart';
import 'adjacent_builder.dart';
import 'chain_builder.dart';
import 'comment_writer.dart';
import 'delimited_list_builder.dart';
import 'piece_factory.dart';
import 'piece_writer.dart';
import 'sequence_builder.dart';
/// Visits every token of the AST and produces a tree of [Piece]s that
/// corresponds to it and contains every token and comment in the original
/// source.
///
/// To avoid this class becoming a monolith, functionality is divided into a
/// couple of mixins, one for each area of functionality. This class then
/// contains only shared state and the visitor methods for the AST.
class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
@override
final PieceWriter pieces;
@override
final CommentWriter comments;
/// Create a new visitor that will be called to visit the code in [source].
factory AstNodeVisitor(
DartFormatter formatter, LineInfo lineInfo, SourceCode source) {
var comments = CommentWriter(lineInfo);
var pieces = PieceWriter(formatter, source, comments);
return AstNodeVisitor._(pieces, comments);
}
AstNodeVisitor._(this.pieces, this.comments);
/// Visits [node] and returns the formatted result.
///
/// Returns a [SourceCode] containing the resulting formatted source and
/// updated selection, if any.
///
/// This is the only method that should be called externally. Everything else
/// is effectively private.
SourceCode run(AstNode node) {
// Always treat the code being formatted as contained in a sequence, even
// if we aren't formatting an entire compilation unit. That way, comments
// before and after the node are handled properly.
var sequence = SequenceBuilder(this);
if (node is CompilationUnit) {
if (node.scriptTag case var scriptTag?) {
sequence.visit(scriptTag);
sequence.addBlank();
}
// Put a blank line between the library tag and the other directives.
Iterable<Directive> directives = node.directives;
if (directives.isNotEmpty && directives.first is LibraryDirective) {
sequence.visit(directives.first);
sequence.addBlank();
directives = directives.skip(1);
}
for (var directive in directives) {
sequence.visit(directive);
}
for (var declaration in node.declarations) {
var hasBody = declaration is ClassDeclaration ||
declaration is EnumDeclaration ||
declaration is ExtensionDeclaration;
// Add a blank line before types with bodies.
if (hasBody) sequence.addBlank();
sequence.visit(declaration);
// Add a blank line after type or function declarations with bodies.
if (hasBody || declaration.hasNonEmptyBody) sequence.addBlank();
}
} else {
// Just formatting a single statement.
sequence.visit(node);
}
// Write any comments at the end of the code.
sequence.addCommentsBefore(node.endToken.next!);
// Finish writing and return the complete result.
return pieces.finish(sequence.build());
}
@override
Piece visitAdjacentStrings(AdjacentStrings node) {
return AdjacentStringsPiece(node.strings.map(nodePiece).toList(),
indent: node.indentStrings);
}
@override
Piece visitAnnotation(Annotation node) {
throw UnimplementedError();
}
@override
Piece visitArgumentList(ArgumentList node) {
return createArgumentList(
node.leftParenthesis, node.arguments, node.rightParenthesis);
}
@override
Piece visitAsExpression(AsExpression node) {
return createInfix(node.expression, node.asOperator, node.type);
}
@override
Piece visitAssertInitializer(AssertInitializer node) {
return buildPiece((b) {
b.token(node.assertKeyword);
b.add(createList(
leftBracket: node.leftParenthesis,
[
node.condition,
if (node.message case var message?) message,
],
rightBracket: node.rightParenthesis,
));
});
}
@override
Piece visitAssertStatement(AssertStatement node) {
return buildPiece((b) {
b.token(node.assertKeyword);
b.add(createArgumentList(
node.leftParenthesis,
[
node.condition,
if (node.message case var message?) message,
],
node.rightParenthesis));
b.token(node.semicolon);
});
}
@override
Piece visitAssignedVariablePattern(AssignedVariablePattern node) {
throw UnimplementedError();
}
@override
Piece visitAssignmentExpression(AssignmentExpression node) {
return createAssignment(
node.leftHandSide, node.operator, node.rightHandSide);
}
@override
Piece visitAwaitExpression(AwaitExpression node) {
return buildPiece((b) {
b.token(node.awaitKeyword);
b.space();
b.visit(node.expression);
});
}
@override
Piece visitBinaryExpression(BinaryExpression node) {
return createInfixChain<BinaryExpression>(
node,
precedence: node.operator.type.precedence,
(expression) => (
expression.leftOperand,
expression.operator,
expression.rightOperand
));
}
@override
Piece visitBlock(Block node) {
return createBlock(node);
}
@override
Piece visitBlockFunctionBody(BlockFunctionBody node) {
return buildPiece((b) {
functionBodyModifiers(node, b);
b.visit(node.block);
});
}
@override
Piece visitBooleanLiteral(BooleanLiteral node) {
return tokenPiece(node.literal);
}
@override
Piece visitBreakStatement(BreakStatement node) {
return createBreak(node.breakKeyword, node.label, node.semicolon);
}
@override
Piece visitCascadeExpression(CascadeExpression node) {
return ChainBuilder(this, node).build();
}
@override
Piece visitCaseClause(CaseClause node) {
return buildPiece((b) {
b.token(node.caseKeyword);
if (node.guardedPattern.whenClause != null) {
throw UnimplementedError();
}
b.space();
b.visit(node.guardedPattern.pattern);
});
}
@override
Piece visitCastPattern(CastPattern node) {
throw UnimplementedError();
}
@override
Piece visitCatchClause(CatchClause node) {
throw UnsupportedError('This node is handled by visitTryStatement().');
}
@override
Piece visitCatchClauseParameter(CatchClauseParameter node) {
return tokenPiece(node.name);
}
@override
Piece visitClassDeclaration(ClassDeclaration node) {
return createType(
node.metadata,
[
node.abstractKeyword,
node.baseKeyword,
node.interfaceKeyword,
node.finalKeyword,
node.sealedKeyword,
node.macroKeyword,
node.mixinKeyword,
],
node.classKeyword,
node.name,
typeParameters: node.typeParameters,
extendsClause: node.extendsClause,
withClause: node.withClause,
implementsClause: node.implementsClause,
nativeClause: node.nativeClause,
body: (
leftBracket: node.leftBracket,
members: node.members,
rightBracket: node.rightBracket
));
}
@override
Piece visitClassTypeAlias(ClassTypeAlias node) {
return createType(
node.metadata,
[
node.abstractKeyword,
node.baseKeyword,
node.interfaceKeyword,
node.finalKeyword,
node.sealedKeyword,
node.mixinKeyword,
],
node.typedefKeyword,
node.name,
equals: node.equals,
superclass: node.superclass,
typeParameters: node.typeParameters,
withClause: node.withClause,
implementsClause: node.implementsClause,
semicolon: node.semicolon);
}
@override
Piece visitComment(Comment node) {
throw UnsupportedError('Comments should be handled elsewhere.');
}
@override
Piece visitCommentReference(CommentReference node) {
throw UnsupportedError('Comments should be handled elsewhere.');
}
@override
Piece visitCompilationUnit(CompilationUnit node) {
throw UnsupportedError(
'CompilationUnit should be handled directly by format().');
}
@override
Piece visitConditionalExpression(ConditionalExpression node) {
var condition = nodePiece(node.condition);
var thenPiece = buildPiece((b) {
b.token(node.question);
b.space();
b.visit(node.thenExpression);
});
var elsePiece = buildPiece((b) {
b.token(node.colon);
b.space();
b.visit(node.elseExpression);
});
var piece = InfixPiece([condition, thenPiece, elsePiece]);
// If conditional expressions are directly nested, force them all to split,
// both parents and children.
if (node.parent is ConditionalExpression ||
node.thenExpression is ConditionalExpression ||
node.elseExpression is ConditionalExpression) {
piece.pin(State.split);
}
return piece;
}
@override
Piece visitConfiguration(Configuration node) {
return buildPiece((b) {
b.token(node.ifKeyword);
b.space();
b.token(node.leftParenthesis);
if (node.equalToken case var equals?) {
b.add(createInfix(node.name, equals, node.value!, hanging: true));
} else {
b.visit(node.name);
}
b.token(node.rightParenthesis);
b.space();
b.visit(node.uri);
});
}
@override
Piece visitConstantPattern(ConstantPattern node) {
if (node.constKeyword != null) throw UnimplementedError();
return nodePiece(node.expression);
}
@override
Piece visitConstructorDeclaration(ConstructorDeclaration node) {
var header = buildPiece((b) {
b.modifier(node.externalKeyword);
b.modifier(node.constKeyword);
b.modifier(node.factoryKeyword);
b.visit(node.returnType);
b.token(node.period);
b.token(node.name);
});
var parameters = nodePiece(node.parameters);
Piece? redirect;
Piece? initializerSeparator;
Piece? initializers;
if (node.redirectedConstructor case var constructor?) {
redirect = AssignPiece(
tokenPiece(node.separator!), nodePiece(constructor),
allowInnerSplit: false);
} else if (node.initializers.isNotEmpty) {
initializerSeparator = tokenPiece(node.separator!);
initializers = createList(node.initializers,
style: const ListStyle(commas: Commas.nonTrailing));
}
var body = createFunctionBody(node.body);
return ConstructorPiece(header, parameters, body,
canSplitParameters: node.parameters.parameters
.canSplit(node.parameters.rightParenthesis),
hasOptionalParameter: node.parameters.rightDelimiter != null,
redirect: redirect,
initializerSeparator: initializerSeparator,
initializers: initializers);
}
@override
Piece visitConstructorFieldInitializer(ConstructorFieldInitializer node) {
return buildPiece((b) {
b.token(node.thisKeyword);
b.token(node.period);
b.add(createAssignment(node.fieldName, node.equals, node.expression));
});
}
@override
Piece visitConstructorName(ConstructorName node) {
var builder = AdjacentBuilder(this);
if (node.type.importPrefix case var importPrefix?) {
builder.token(importPrefix.name);
builder.token(importPrefix.period);
}
// The name of the type being constructed.
var type = node.type;
builder.token(type.name2);
builder.visit(type.typeArguments);
builder.token(type.question);
// If this is a named constructor, the name.
if (node.name != null) {
builder.token(node.period);
builder.visit(node.name);
}
// If there was a prefix or constructor name, then make a splittable piece.
// Otherwise, the current piece is a simple identifier for the name.
return builder.build();
}
@override
Piece visitContinueStatement(ContinueStatement node) {
return createBreak(node.continueKeyword, node.label, node.semicolon);
}
@override
Piece visitDeclaredIdentifier(DeclaredIdentifier node) {
return buildPiece((b) {
b.modifier(node.keyword);
b.visit(node.type, spaceAfter: true);
b.token(node.name);
});
}
@override
Piece visitDeclaredVariablePattern(DeclaredVariablePattern node) {
var header = buildPiece((b) {
b.modifier(node.keyword);
b.visit(node.type);
});
return VariablePiece(
header,
[tokenPiece(node.name)],
hasType: node.type != null,
);
}
@override
Piece visitDefaultFormalParameter(DefaultFormalParameter node) {
if (node.separator case var separator?) {
return createAssignment(node.parameter, separator, node.defaultValue!,
spaceBeforeOperator: separator.type == TokenType.EQ);
} else {
return nodePiece(node.parameter);
}
}
@override
Piece visitDoStatement(DoStatement node) {
return buildPiece((b) {
b.token(node.doKeyword);
b.space();
b.visit(node.body);
b.space();
b.token(node.whileKeyword);
b.space();
b.token(node.leftParenthesis);
b.visit(node.condition);
b.token(node.rightParenthesis);
b.token(node.semicolon);
});
}
@override
Piece visitDottedName(DottedName node) {
return createDotted(node.components);
}
@override
Piece visitDoubleLiteral(DoubleLiteral node) {
return tokenPiece(node.literal);
}
@override
Piece visitEmptyFunctionBody(EmptyFunctionBody node) {
return tokenPiece(node.semicolon);
}
@override
Piece visitEmptyStatement(EmptyStatement node) {
return tokenPiece(node.semicolon);
}
@override
Piece visitEnumConstantDeclaration(EnumConstantDeclaration node) {
return createEnumConstant(node);
}
@override
Piece visitEnumDeclaration(EnumDeclaration node) {
if (node.metadata.isNotEmpty) throw UnimplementedError();
var header = buildPiece((b) {
b.token(node.enumKeyword);
b.space();
b.token(node.name);
b.visit(node.typeParameters);
});
if (node.members.isEmpty) {
// If there are no members, format the constants like a delimited list.
// This keeps the enum declaration on one line if it fits.
var builder = DelimitedListBuilder(
this,
const ListStyle(
spaceWhenUnsplit: true, splitListIfBeforeSplits: true));
builder.leftBracket(node.leftBracket, preceding: header);
node.constants.forEach(builder.visit);
builder.rightBracket(semicolon: node.semicolon, node.rightBracket);
return builder.build();
} else {
var builder = AdjacentBuilder(this);
builder.add(header);
builder.space();
// If there are members, format it like a block where each constant and
// member is on its own line.
var sequence = SequenceBuilder(this);
sequence.leftBracket(node.leftBracket);
for (var constant in node.constants) {
sequence.addCommentsBefore(constant.firstNonCommentToken);
sequence.add(createEnumConstant(constant,
hasMembers: true,
isLastConstant: constant == node.constants.last,
semicolon: node.semicolon));
}
// Insert a blank line between the constants and members.
sequence.addBlank();
for (var node in node.members) {
sequence.visit(node);
// If the node has a non-empty braced body, then require a blank line
// between it and the next node.
if (node.hasNonEmptyBody) sequence.addBlank();
}
sequence.rightBracket(node.rightBracket);
builder.add(sequence.build());
return builder.build();
}
}
@override
Piece visitExportDirective(ExportDirective node) {
return createImport(node, node.exportKeyword);
}
@override
Piece visitExpressionFunctionBody(ExpressionFunctionBody node) {
return buildPiece((b) {
var operatorPiece = buildPiece((b) {
functionBodyModifiers(node, b);
b.token(node.functionDefinition);
});
var expression = nodePiece(node.expression);
b.add(AssignPiece(operatorPiece, expression,
allowInnerSplit: node.expression.canBlockSplit));
b.token(node.semicolon);
});
}
@override
Piece visitExpressionStatement(ExpressionStatement node) {
return buildPiece((b) {
b.visit(node.expression);
b.token(node.semicolon);
});
}
@override
Piece visitExtendsClause(ExtendsClause node) {
throw UnsupportedError(
'This node is handled by PieceFactory.createType().');
}
@override
Piece visitExtensionDeclaration(ExtensionDeclaration node) {
return createType(node.metadata, const [], node.extensionKeyword, node.name,
typeParameters: node.typeParameters,
onType: (node.onKeyword, node.extendedType),
body: (
leftBracket: node.leftBracket,
members: node.members,
rightBracket: node.rightBracket
));
}
@override
Piece visitFieldDeclaration(FieldDeclaration node) {
return buildPiece((b) {
b.modifier(node.externalKeyword);
b.modifier(node.staticKeyword);
b.modifier(node.abstractKeyword);
b.modifier(node.covariantKeyword);
b.visit(node.fields);
b.token(node.semicolon);
});
}
@override
Piece visitFieldFormalParameter(FieldFormalParameter node) {
if (node.parameters case var parameters?) {
// A function-typed field formal like:
//
// C(this.fn(parameter));
return createFunctionType(
node.type,
fieldKeyword: node.thisKeyword,
period: node.period,
node.name,
node.typeParameters,
parameters,
node.question,
parameter: node);
} else {
return createFormalParameter(
node,
mutableKeyword: node.keyword,
fieldKeyword: node.thisKeyword,
period: node.period,
node.type,
node.name);
}
}
@override
Piece visitFormalParameterList(FormalParameterList node) {
// Find the first non-mandatory parameter (if there are any).
var firstOptional =
node.parameters.indexWhere((p) => p is DefaultFormalParameter);
// If all parameters are optional, put the `[` or `{` right after `(`.
var builder = DelimitedListBuilder(this);
if (node.parameters.isNotEmpty && firstOptional == 0) {
builder.leftBracket(node.leftParenthesis, delimiter: node.leftDelimiter);
} else {
builder.leftBracket(node.leftParenthesis);
}
for (var i = 0; i < node.parameters.length; i++) {
// If this is the first optional parameter, put the delimiter before it.
if (firstOptional > 0 && i == firstOptional) {
builder.leftDelimiter(node.leftDelimiter!);
}
builder.visit(node.parameters[i]);
}
builder.rightBracket(node.rightParenthesis, delimiter: node.rightDelimiter);
return builder.build();
}
@override
Piece visitForElement(ForElement node) {
var forKeyword = buildPiece((b) {
b.modifier(node.awaitKeyword);
b.token(node.forKeyword);
});
var forPartsPiece = createForLoopParts(
node.leftParenthesis, node.forLoopParts, node.rightParenthesis);
var body = nodePiece(node.body);
var forPiece = ForPiece(forKeyword, forPartsPiece, body,
hasBlockBody: node.body.isSpreadCollection);
// It looks weird to have multiple nested control flow elements on the same
// line, so force the outer one to split if there is an inner one.
if (node.body.isControlFlowElement) {
forPiece.pin(State.split);
}
return forPiece;
}
@override
Piece visitForStatement(ForStatement node) {
var forKeyword = buildPiece((b) {
b.modifier(node.awaitKeyword);
b.token(node.forKeyword);
});
var forPartsPiece = createForLoopParts(
node.leftParenthesis, node.forLoopParts, node.rightParenthesis);
var body = nodePiece(node.body);
return ForPiece(forKeyword, forPartsPiece, body,
hasBlockBody: node.body is Block);
}
@override
Piece visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) {
throw UnsupportedError('This node is handled by visitForStatement().');
}
@override
Piece visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) {
throw UnsupportedError('This node is handled by visitForStatement().');
}
@override
Piece visitForEachPartsWithPattern(ForEachPartsWithPattern node) {
throw UnsupportedError('This node is handled by visitForStatement().');
}
@override
Piece visitForPartsWithDeclarations(ForPartsWithDeclarations node) {
throw UnsupportedError('This node is handled by visitForStatement().');
}
@override
Piece visitForPartsWithExpression(ForPartsWithExpression node) {
throw UnsupportedError('This node is handled by visitForStatement().');
}
@override
Piece visitForPartsWithPattern(ForPartsWithPattern node) {
throw UnsupportedError('This node is handled by visitForStatement().');
}
@override
Piece visitFunctionDeclaration(FunctionDeclaration node) {
return createFunction(
modifiers: [node.externalKeyword],
returnType: node.returnType,
propertyKeyword: node.propertyKeyword,
name: node.name,
typeParameters: node.functionExpression.typeParameters,
parameters: node.functionExpression.parameters,
body: node.functionExpression.body);
}
@override
Piece visitFunctionDeclarationStatement(FunctionDeclarationStatement node) {
return nodePiece(node.functionDeclaration);
}
@override
Piece visitFunctionExpression(FunctionExpression node) {
return createFunction(
typeParameters: node.typeParameters,
parameters: node.parameters,
body: node.body);
}
@override
Piece visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
// TODO(tall): This is just basic support to get the syntax doing something
// so that tests of other features that happen to use this syntax can run.
// The main tests for function expression calls still need to be migrated
// over and this may need some tweaks.
return buildPiece((b) {
b.visit(node.function);
b.visit(node.typeArguments);
b.visit(node.argumentList);
});
}
@override
Piece visitFunctionReference(FunctionReference node) {
return buildPiece((b) {
b.visit(node.function);
b.visit(node.typeArguments);
});
}
@override
Piece visitFunctionTypeAlias(FunctionTypeAlias node) {
if (node.metadata.isNotEmpty) throw UnimplementedError();
return buildPiece((b) {
b.token(node.typedefKeyword);
b.space();
b.token(node.name);
b.visit(node.typeParameters);
b.visit(node.parameters);
b.token(node.semicolon);
});
}
@override
Piece visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) {
return createFunctionType(
parameter: node,
node.returnType,
node.name,
node.typeParameters,
node.parameters,
node.question);
}
@override
Piece visitGenericFunctionType(GenericFunctionType node) {
return createFunctionType(node.returnType, node.functionKeyword,
node.typeParameters, node.parameters, node.question);
}
@override
Piece visitGenericTypeAlias(GenericTypeAlias node) {
if (node.metadata.isNotEmpty) throw UnimplementedError();
return buildPiece((b) {
b.token(node.typedefKeyword);
b.space();
b.token(node.name);
b.visit(node.typeParameters);
b.space();
b.token(node.equals);
// Don't bother allowing splitting after the `=`. It's always better to
// split inside the type parameter, type argument, or parameter lists of
// the typedef or the aliased type.
b.space();
b.visit(node.type);
b.token(node.semicolon);
});
}
@override
Piece visitHideCombinator(HideCombinator node) {
throw UnsupportedError('Combinators are handled by createImport().');
}
@override
Piece visitIfElement(IfElement node) {
var piece = IfPiece();
// Recurses through the else branches to flatten them into a linear if-else
// chain handled by a single [IfPiece].
void traverse(Token? precedingElse, IfElement ifElement) {
var spreadThen = ifElement.thenElement.spreadCollection;
var condition = buildPiece((b) {
b.token(precedingElse, spaceAfter: true);
b.add(startControlFlow(ifElement.ifKeyword, ifElement.leftParenthesis,
ifElement.expression, ifElement.rightParenthesis));
// Make the `...` part of the header so that IfPiece can correctly
// constrain the inner collection literal's ListPiece to split.
if (spreadThen != null) {
b.space();
b.token(spreadThen.spreadOperator);
}
});
Piece thenElement;
if (spreadThen != null) {
thenElement = nodePiece(spreadThen.expression);
} else {
thenElement = nodePiece(ifElement.thenElement);
}
// If the then branch of an if element is itself a control flow
// element, then force the outer if to always split.
if (ifElement.thenElement.isControlFlowElement) {
piece.pin(State.split);
}
piece.add(condition, thenElement, isBlock: spreadThen != null);
switch (ifElement.elseElement) {
case IfElement elseIf:
// Hit an else-if, so flatten it into the chain with the `else`
// becoming part of the next section's header.
traverse(ifElement.elseKeyword, elseIf);
case var elseElement?:
var spreadElse = elseElement.spreadCollection;
// Any other kind of else body ends the chain, with the header for
// the last section just being the `else` keyword.
var header = buildPiece((b) {
b.token(ifElement.elseKeyword!);
// Make the `...` part of the header so that IfPiece can correctly
// constrain the inner collection literal's ListPiece to split.
if (spreadElse != null) {
b.space();
b.token(spreadElse.spreadOperator);
}
});
Piece statement;
if (spreadElse != null) {
statement = nodePiece(spreadElse.expression);
} else {
statement = nodePiece(elseElement);
}
piece.add(header, statement, isBlock: spreadElse != null);
// If the else branch of an if element is itself a control flow
// element, then force the outer if to always split.
if (ifElement.thenElement.isControlFlowElement) {
piece.pin(State.split);
}
case null:
break; // Nothing to do.
}
}
traverse(null, node);
return piece;
}
@override
Piece visitIfStatement(IfStatement node) {
var piece = IfPiece();
// Recurses through the else branches to flatten them into a linear if-else
// chain handled by a single [IfPiece].
void traverse(Token? precedingElse, IfStatement ifStatement) {
var condition = buildPiece((b) {
b.token(precedingElse, spaceAfter: true);
b.token(ifStatement.ifKeyword);
b.space();
b.token(ifStatement.leftParenthesis);
// If the condition needs to split, we prefer splitting before the
// `case` keyword, like:
//
// if (obj
// case 123456789012345678901234567890) {
// body;
// }
var expressionPiece = nodePiece(ifStatement.expression);
if (ifStatement.caseClause case var caseClause?) {
var caseClausePiece = nodePiece(caseClause);
// If the case clause can have block formatting, then a newline in
// it doesn't force the if-case to split before the `case` keyword,
// like:
//
// if (obj case [
// first,
// second,
// third,
// ]) {
// ;
// }
var allowInnerSplit = caseClause.guardedPattern.pattern.canBlockSplit;
b.add(AssignPiece(
expressionPiece,
caseClausePiece,
allowInnerSplit: allowInnerSplit,
indentInValue: true,
));
} else {
b.add(expressionPiece);
}
b.token(ifStatement.rightParenthesis);
b.space();
});
// Edge case: When the then branch is a block and there is an else clause
// after it, we want to force the block to split even if empty, like:
//
// if (condition) {
// } else {
// body;
// }
var thenStatement = switch (ifStatement.thenStatement) {
Block thenBlock when ifStatement.elseStatement != null =>
createBlock(thenBlock, forceSplit: true),
_ => nodePiece(ifStatement.thenStatement)
};
piece.add(condition, thenStatement,
isBlock: ifStatement.thenStatement is Block);
switch (ifStatement.elseStatement) {
case IfStatement elseIf:
// Hit an else-if, so flatten it into the chain with the `else`
// becoming part of the next section's header.
traverse(ifStatement.elseKeyword, elseIf);
case var elseStatement?:
// Any other kind of else body ends the chain, with the header for
// the last section just being the `else` keyword.
var header = buildPiece((b) {
b.token(ifStatement.elseKeyword, spaceAfter: true);
});
var statement = nodePiece(elseStatement);
piece.add(header, statement, isBlock: elseStatement is Block);
}
}
traverse(null, node);
// If statements almost always split at the clauses unless the if is a
// simple if with only a single unbraced then statement and no else clause,
// like:
//
// if (condition) print("ok");
if (node.thenStatement is Block || node.elseStatement != null) {
piece.pin(State.split);
}
return piece;
}
@override
Piece visitImplementsClause(ImplementsClause node) {
throw UnsupportedError(
'This node is handled by PieceFactory.createType().');
}
@override
Piece visitImportDirective(ImportDirective node) {
return createImport(node, node.importKeyword,
deferredKeyword: node.deferredKeyword,
asKeyword: node.asKeyword,
prefix: node.prefix);
}
@override
Piece visitIndexExpression(IndexExpression node) {
Piece? targetPiece;
if (node.target case var target?) targetPiece = nodePiece(target);
return createIndexExpression(targetPiece, node);
}
@override
Piece visitInstanceCreationExpression(InstanceCreationExpression node) {
var builder = AdjacentBuilder(this);
builder.token(node.keyword, spaceAfter: true);
var constructor = node.constructorName;
if (constructor.type.importPrefix case var importPrefix?) {
builder.token(importPrefix.name);
builder.token(importPrefix.period);
}
// The type being constructed.
var type = constructor.type;
builder.token(type.name2);
builder.visit(type.typeArguments);
// If this is a named constructor call, the name.
if (constructor.name case var name?) {
builder.token(constructor.period);
builder.visit(name);
}
builder.visit(node.argumentList);
return builder.build();
}
@override
Piece visitIntegerLiteral(IntegerLiteral node) {
return tokenPiece(node.literal);
}
@override
Piece visitInterpolationExpression(InterpolationExpression node) {
return buildPiece((b) {
b.token(node.leftBracket);
b.visit(node.expression);
b.token(node.rightBracket);
});
}
@override
Piece visitInterpolationString(InterpolationString node) {
return pieces.stringLiteralPiece(node.contents,
isMultiline: (node.parent as StringInterpolation).isMultiline);
}
@override
Piece visitIsExpression(IsExpression node) {
return createInfix(
node.expression,
node.isOperator,
operator2: node.notOperator,
node.type);
}
@override
Piece visitLabel(Label node) {
return buildPiece((b) {
b.visit(node.label);
b.token(node.colon);
});
}
@override
Piece visitLabeledStatement(LabeledStatement node) {
var sequence = SequenceBuilder(this);
for (var label in node.labels) {
sequence.visit(label);
}
sequence.visit(node.statement);
return sequence.build();
}
@override
Piece visitLibraryDirective(LibraryDirective node) {
return buildPiece((b) {
createDirectiveMetadata(node);
b.token(node.libraryKeyword);
b.visit(node.name2, spaceBefore: true);
b.token(node.semicolon);
});
}
@override
Piece visitLibraryIdentifier(LibraryIdentifier node) {
return createDotted(node.components);
}
@override
Piece visitListLiteral(ListLiteral node) {
return createCollection(
constKeyword: node.constKeyword,
typeArguments: node.typeArguments,
node.leftBracket,
node.elements,
node.rightBracket,
);
}
@override
Piece visitListPattern(ListPattern node) {
return createCollection(
typeArguments: node.typeArguments,
node.leftBracket,
node.elements,
node.rightBracket,
);
}
@override
Piece visitLogicalAndPattern(LogicalAndPattern node) {
throw UnimplementedError();
}
@override
Piece visitLogicalOrPattern(LogicalOrPattern node) {
throw UnimplementedError();
}
@override
Piece visitMapLiteralEntry(MapLiteralEntry node) {
return createAssignment(node.key, node.separator, node.value,
spaceBeforeOperator: false);
}
@override
Piece visitMapPattern(MapPattern node) {
return createCollection(
typeArguments: node.typeArguments,
node.leftBracket,
node.elements,
node.rightBracket,
);
}
@override
Piece visitMapPatternEntry(MapPatternEntry node) {
return createAssignment(node.key, node.separator, node.value,
spaceBeforeOperator: false);
}
@override
Piece visitMethodDeclaration(MethodDeclaration node) {
return createFunction(
modifiers: [node.externalKeyword, node.modifierKeyword],
returnType: node.returnType,
propertyKeyword: node.operatorKeyword ?? node.propertyKeyword,
name: node.name,
typeParameters: node.typeParameters,
parameters: node.parameters,
body: node.body);
}
@override
Piece visitMethodInvocation(MethodInvocation node) {
// If there's no target, this is a "bare" function call like "foo(1, 2)",
// or a section in a cascade.
//
// If it looks like a constructor or static call, we want to keep the
// target and method together instead of including the method in the
// subsequent method chain.
if (node.target == null || node.looksLikeStaticCall) {
return buildPiece((b) {
b.visit(node.target);
b.token(node.operator);
b.visit(node.methodName);
b.visit(node.typeArguments);
b.visit(node.argumentList);
});
}
return ChainBuilder(this, node).build();
}
@override
Piece visitMixinDeclaration(MixinDeclaration node) {
return createType(
node.metadata, [node.baseKeyword], node.mixinKeyword, node.name,
typeParameters: node.typeParameters,
onClause: node.onClause,
implementsClause: node.implementsClause,
body: (
leftBracket: node.leftBracket,
members: node.members,
rightBracket: node.rightBracket
));
}
@override
Piece visitNamedExpression(NamedExpression node) {
return createAssignment(node.name.label, node.name.colon, node.expression,
spaceBeforeOperator: false);
}
@override
Piece visitNamedType(NamedType node) {
return buildPiece((b) {
b.token(node.importPrefix?.name);
b.token(node.importPrefix?.period);
b.token(node.name2);
b.visit(node.typeArguments);
b.token(node.question);
});
}
@override
Piece visitNativeClause(NativeClause node) {
return buildPiece((b) {
b.token(node.nativeKeyword);
b.visit(node.name, spaceBefore: true);
});
}
@override
Piece visitNativeFunctionBody(NativeFunctionBody node) {
return buildPiece((b) {
b.token(node.nativeKeyword);
b.visit(node.stringLiteral, spaceBefore: true);
b.token(node.semicolon);
});
}
@override
Piece visitNullAssertPattern(NullAssertPattern node) {
throw UnimplementedError();
}
@override
Piece visitNullCheckPattern(NullCheckPattern node) {
throw UnimplementedError();
}
@override
Piece visitNullLiteral(NullLiteral node) {
return tokenPiece(node.literal);
}
@override
Piece visitObjectPattern(ObjectPattern node) {
throw UnimplementedError();
}
@override
Piece visitOnClause(OnClause node) {
throw UnsupportedError(
'This node is handled by PieceFactory.createType().');
}
@override
Piece visitParenthesizedExpression(ParenthesizedExpression node) {
return buildPiece((b) {
b.token(node.leftParenthesis);
b.visit(node.expression);
b.token(node.rightParenthesis);
});
}
@override
Piece visitParenthesizedPattern(ParenthesizedPattern node) {
throw UnimplementedError();
}
@override
Piece visitPartDirective(PartDirective node) {
return buildPiece((b) {
createDirectiveMetadata(node);
b.token(node.partKeyword);
b.space();
b.visit(node.uri);
b.token(node.semicolon);
});
}
@override
Piece visitPartOfDirective(PartOfDirective node) {
return buildPiece((b) {
createDirectiveMetadata(node);
b.token(node.partKeyword);
b.space();
b.token(node.ofKeyword);
b.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.
b.visit(node.libraryName);
b.visit(node.uri);
b.token(node.semicolon);
});
}
@override
Piece visitPatternAssignment(PatternAssignment node) {
throw UnimplementedError();
}
@override
Piece visitPatternField(PatternField node) {
throw UnimplementedError();
}
@override
Piece visitPatternVariableDeclaration(PatternVariableDeclaration node) {
throw UnimplementedError();
}
@override
Piece visitPatternVariableDeclarationStatement(
PatternVariableDeclarationStatement node) {
throw UnimplementedError();
}
@override
Piece visitPostfixExpression(PostfixExpression node) {
return buildPiece((b) {
b.visit(node.operand);
b.token(node.operator);
});
}
@override
Piece visitPrefixedIdentifier(PrefixedIdentifier node) {
return ChainBuilder(this, node).build();
}
@override
Piece visitPrefixExpression(PrefixExpression node) {
return buildPiece((b) {
b.token(node.operator);
// Edge case: put a space after "-" if the operand is "-" or "--" so that
// we don't merge the operator tokens.
if (node.operand
case PrefixExpression(operator: Token(lexeme: '-' || '--'))) {
b.space();
}
b.visit(node.operand);
});
}
@override
Piece visitPropertyAccess(PropertyAccess node) {
// If there's no target, this is a section in a cascade.
if (node.target == null) {
return buildPiece((b) {
b.token(node.operator);
b.visit(node.propertyName);
});
}
return ChainBuilder(this, node).build();
}
@override
Piece visitRedirectingConstructorInvocation(
RedirectingConstructorInvocation node) {
return buildPiece((b) {
b.token(node.thisKeyword);
b.token(node.period);
b.visit(node.constructorName);
b.visit(node.argumentList);
});
}
@override
Piece visitRecordLiteral(RecordLiteral node) {
ListStyle style;
if (node.fields.length == 1 && node.fields[0] is! NamedExpression) {
// Single-element records always have a trailing comma, unless the single
// element is a named field.
style = const ListStyle(commas: Commas.alwaysTrailing);
} else {
style = const ListStyle(commas: Commas.trailing);
}
return createCollection(
constKeyword: node.constKeyword,
node.leftParenthesis,
node.fields,
node.rightParenthesis,
style: style,
);
}
@override
Piece visitRecordPattern(RecordPattern node) {
throw UnimplementedError();
}
@override
Piece visitRecordTypeAnnotation(RecordTypeAnnotation node) {
var namedFields = node.namedFields;
var positionalFields = node.positionalFields;
// Single positional record types always have a trailing comma.
var listStyle = positionalFields.length == 1 && namedFields == null
? const ListStyle(commas: Commas.alwaysTrailing)
: const ListStyle(commas: Commas.trailing);
var builder = DelimitedListBuilder(this, listStyle);
// If all parameters are optional, put the `{` right after `(`.
if (positionalFields.isEmpty && namedFields != null) {
builder.leftBracket(
node.leftParenthesis,
delimiter: namedFields.leftBracket,
);
} else {
builder.leftBracket(node.leftParenthesis);
}
for (var positionalField in positionalFields) {
builder.visit(positionalField);
}
Token? rightDelimiter;
if (namedFields != null) {
// If we have both positional fields and named fields, then we need to add
// the left bracket delimiter before the first named field.
if (positionalFields.isNotEmpty) {
builder.leftDelimiter(namedFields.leftBracket);
}
for (var namedField in namedFields.fields) {
builder.visit(namedField);
}
rightDelimiter = namedFields.rightBracket;
}
builder.rightBracket(node.rightParenthesis, delimiter: rightDelimiter);
return buildPiece((b) {
b.add(builder.build());
b.token(node.question);
});
}
@override
Piece visitRecordTypeAnnotationNamedField(
RecordTypeAnnotationNamedField node) {
return createRecordTypeField(node);
}
@override
Piece visitRecordTypeAnnotationPositionalField(
RecordTypeAnnotationPositionalField node) {
return createRecordTypeField(node);
}
@override
Piece visitRelationalPattern(RelationalPattern node) {
throw UnimplementedError();
}
@override
Piece visitRethrowExpression(RethrowExpression node) {
return tokenPiece(node.rethrowKeyword);
}
@override
Piece visitRestPatternElement(RestPatternElement node) {
return buildPiece((b) {
b.token(node.operator);
b.visit(node.pattern);
});
}
@override
Piece visitReturnStatement(ReturnStatement node) {
return buildPiece((b) {
b.token(node.returnKeyword);
b.visit(node.expression, spaceBefore: true);
b.token(node.semicolon);
});
}
@override
Piece visitScriptTag(ScriptTag node) {
// The lexeme includes the trailing newline. Strip it off since the
// formatter ensures it gets a newline after it.
return tokenPiece(node.scriptTag, lexeme: node.scriptTag.lexeme.trim());
}
@override
Piece visitSetOrMapLiteral(SetOrMapLiteral node) {
return createCollection(
constKeyword: node.constKeyword,
typeArguments: node.typeArguments,
node.leftBracket,
node.elements,
node.rightBracket,
);
}
@override
Piece visitShowCombinator(ShowCombinator node) {
throw UnsupportedError('Combinators are handled by createImport().');
}
@override
Piece visitSimpleFormalParameter(SimpleFormalParameter node) {
return createFormalParameter(node, node.type, node.name,
mutableKeyword: node.keyword);
}
@override
Piece visitSimpleIdentifier(SimpleIdentifier node) {
return tokenPiece(node.token);
}
@override
Piece visitSimpleStringLiteral(SimpleStringLiteral node) {
return pieces.stringLiteralPiece(node.literal,
isMultiline: node.isMultiline);
}
@override
Piece visitSpreadElement(SpreadElement node) {
return buildPiece((b) {
b.token(node.spreadOperator);
b.visit(node.expression);
});
}
@override
Piece visitStringInterpolation(StringInterpolation node) {
return buildPiece((b) {
for (var element in node.elements) {
b.visit(element);
}
});
}
@override
Piece visitSuperConstructorInvocation(SuperConstructorInvocation node) {
return buildPiece((b) {
b.token(node.superKeyword);
b.token(node.period);
b.visit(node.constructorName);
b.visit(node.argumentList);
});
}
@override
Piece visitSuperExpression(SuperExpression node) {
return tokenPiece(node.superKeyword);
}
@override
Piece visitSuperFormalParameter(SuperFormalParameter node) {
if (node.parameters case var parameters?) {
// A function-typed super parameter like:
//
// C(super.fn(parameter));
return createFunctionType(
node.type,
fieldKeyword: node.superKeyword,
period: node.period,
node.name,
node.typeParameters,
parameters,
node.question,
parameter: node);
} else {
return createFormalParameter(
node,
mutableKeyword: node.keyword,
fieldKeyword: node.superKeyword,
period: node.period,
node.type,
node.name);
}
}
@override
Piece visitSwitchExpression(SwitchExpression node) {
var value = startControlFlow(node.switchKeyword, node.leftParenthesis,
node.expression, node.rightParenthesis);
var list = DelimitedListBuilder(this,
const ListStyle(spaceWhenUnsplit: true, splitListIfBeforeSplits: true));
list.leftBracket(node.leftBracket, preceding: value);
for (var member in node.cases) {
list.visit(member);
}
list.rightBracket(node.rightBracket);
return list.build();
}
@override
Piece visitSwitchExpressionCase(SwitchExpressionCase node) {
if (node.guardedPattern.whenClause != null) throw UnimplementedError();
return createAssignment(
node.guardedPattern.pattern, node.arrow, node.expression);
}
@override
Piece visitSwitchStatement(SwitchStatement node) {
return buildPiece((b) {
b.add(startControlFlow(node.switchKeyword, node.leftParenthesis,
node.expression, node.rightParenthesis));
b.space();
var sequence = SequenceBuilder(this);
sequence.leftBracket(node.leftBracket);
for (var member in node.members) {
for (var label in member.labels) {
sequence.visit(label);
}
sequence.addCommentsBefore(member.keyword);
var casePiece = buildPiece((b) {
b.token(member.keyword);
if (member is SwitchCase) {
b.space();
b.visit(member.expression);
} else if (member is SwitchPatternCase) {
if (member.guardedPattern.whenClause != null) {
throw UnimplementedError();
}
b.space();
b.visit(member.guardedPattern.pattern);
} else {
assert(member is SwitchDefault);
// Nothing to do.
}
b.token(member.colon);
});
// Don't allow any blank lines between the `case` line and the first
// statement in the case (or the next case if this case has no body).
sequence.add(casePiece, indent: Indent.none, allowBlankAfter: false);
for (var statement in member.statements) {
sequence.visit(statement, indent: Indent.block);
}
}
sequence.rightBracket(node.rightBracket);
b.add(sequence.build());
});
}
@override
Piece visitSymbolLiteral(SymbolLiteral node) {
return buildPiece((b) {
b.token(node.poundSign);
var components = node.components;
for (var component in components) {
// The '.' separator.
if (component != components.first) {
b.token(component.previous!);
}
b.token(component);
}
});
}
@override
Piece visitThisExpression(ThisExpression node) {
return tokenPiece(node.thisKeyword);
}
@override
Piece visitThrowExpression(ThrowExpression node) {
return buildPiece((b) {
b.token(node.throwKeyword);
b.space();
b.visit(node.expression);
});
}
@override
Piece visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
return buildPiece((b) {
b.modifier(node.externalKeyword);
b.visit(node.variables);
b.token(node.semicolon);
});
}
@override
Piece visitTryStatement(TryStatement node) {
return createTry(node);
}
@override
Piece visitTypeArgumentList(TypeArgumentList node) {
return createTypeList(node.leftBracket, node.arguments, node.rightBracket);
}
@override
Piece visitTypeParameter(TypeParameter node) {
return buildPiece((b) {
b.token(node.name);
if (node.bound case var bound?) {
b.space();
b.token(node.extendsKeyword);
b.space();
b.visit(bound);
}
});
}
@override
Piece visitTypeParameterList(TypeParameterList node) {
return createTypeList(
node.leftBracket, node.typeParameters, node.rightBracket);
}
@override
Piece visitVariableDeclaration(VariableDeclaration node) {
throw UnsupportedError('This is handled by visitVariableDeclarationList()');
}
@override
Piece visitVariableDeclarationList(VariableDeclarationList node) {
// TODO(tall): Format metadata.
if (node.metadata.isNotEmpty) throw UnimplementedError();
var header = buildPiece((b) {
b.modifier(node.lateKeyword);
b.modifier(node.keyword);
// TODO(tall): Test how splits inside the type annotation (like in a type
// argument list or a function type's parameter list) affect the
// indentation and splitting of the surrounding variable declaration.
b.visit(node.type);
});
var variables = <Piece>[];
for (var variable in node.variables) {
if ((variable.equals, variable.initializer)
case (var equals?, var initializer?)) {
var variablePiece = buildPiece((b) {
b.token(variable.name);
b.space();
b.token(equals);
});
var initializerPiece = nodePiece(initializer, commaAfter: true);
variables.add(AssignPiece(variablePiece, initializerPiece,
allowInnerSplit: initializer.canBlockSplit));
} else {
variables.add(tokenPiece(variable.name, commaAfter: true));
}
}
return VariablePiece(header, variables, hasType: node.type != null);
}
@override
Piece visitVariableDeclarationStatement(VariableDeclarationStatement node) {
return buildPiece((b) {
b.visit(node.variables);
b.token(node.semicolon);
});
}
@override
Piece visitWhileStatement(WhileStatement node) {
var condition = buildPiece((b) {
b.add(startControlFlow(node.whileKeyword, node.leftParenthesis,
node.condition, node.rightParenthesis));
b.space();
});
var body = nodePiece(node.body);
var piece = IfPiece();
piece.add(condition, body, isBlock: node.body is Block);
return piece;
}
@override
Piece visitWildcardPattern(WildcardPattern node) {
throw UnimplementedError();
}
@override
Piece visitWithClause(WithClause node) {
throw UnsupportedError(
'This node is handled by PieceFactory.createType().');
}
@override
Piece visitYieldStatement(YieldStatement node) {
return buildPiece((b) {
b.token(node.yieldKeyword);
b.token(node.star);
b.space();
b.visit(node.expression);
b.token(node.semicolon);
});
}
/// Visits [node] and creates a piece from it.
///
/// If [commaAfter] is `true`, looks for a comma token after [node] and
/// writes it to the piece as well.
@override
Piece nodePiece(AstNode node, {bool commaAfter = false}) {
var result = node.accept(this)!;
if (commaAfter) {
var nextToken = node.endToken.next!;
if (nextToken.lexeme == ',') {
var comma = tokenPiece(nextToken);
result = AdjacentPiece([result, comma]);
}
}
return result;
}
}