Skip to content

Commit

Permalink
Format for elements. (#1350)
Browse files Browse the repository at this point in the history
Format for elements.

This mostly uses the existing code for formatting the `for (...)` part
of for statements, which I moved into PieceFactory so that it can be
reused.

In the process of adding tests for this, I noticed that a newline in
the initializer of a for-in loop forced the `in` to split, which looks
weird, so I fixed that too, for both for statements and for elements.
  • Loading branch information
munificent authored Jan 8, 2024
1 parent 1ad10d2 commit f88d826
Show file tree
Hide file tree
Showing 10 changed files with 656 additions and 115 deletions.
127 changes: 23 additions & 104 deletions lib/src/front_end/ast_node_visitor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
if (node.redirectedConstructor case var constructor?) {
redirect = AssignPiece(
tokenPiece(node.separator!), nodePiece(constructor),
isValueDelimited: false);
allowInnerSplit: false);
} else if (node.initializers.isNotEmpty) {
initializerSeparator = tokenPiece(node.separator!);
initializers = createList(node.initializers,
Expand Down Expand Up @@ -595,7 +595,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
var expression = nodePiece(node.expression);

b.add(AssignPiece(operatorPiece, expression,
isValueDelimited: node.expression.canBlockSplit));
allowInnerSplit: node.expression.canBlockSplit));
b.token(node.semicolon);
});
}
Expand Down Expand Up @@ -693,117 +693,36 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {

@override
Piece visitForElement(ForElement node) {
throw UnimplementedError();
}

@override
Piece visitForStatement(ForStatement node) {
var forKeyword = buildPiece((b) {
b.modifier(node.awaitKeyword);
b.token(node.forKeyword);
});

Piece forPartsPiece;
switch (node.forLoopParts) {
// Edge case: A totally empty for loop is formatted just as `(;;)` with
// no splits or spaces anywhere.
case ForPartsWithExpression(
initialization: null,
leftSeparator: Token(precedingComments: null),
condition: null,
rightSeparator: Token(precedingComments: null),
updaters: NodeList(isEmpty: true),
) &&
var forParts
when node.rightParenthesis.precedingComments == null:
forPartsPiece = buildPiece((b) {
b.token(node.leftParenthesis);
b.token(forParts.leftSeparator);
b.token(forParts.rightSeparator);
b.token(node.rightParenthesis);
});

case ForParts forParts &&
ForPartsWithDeclarations(variables: AstNode? initializer):
case ForParts forParts &&
ForPartsWithExpression(initialization: AstNode? initializer):
// In a C-style for loop, treat the for loop parts like an argument list
// where each clause is a separate argument. This means that when they
// split, they split like:
//
// for (
// initializerClause;
// conditionClause;
// incrementClause
// ) {
// body;
// }
var partsList =
DelimitedListBuilder(this, const ListStyle(commas: Commas.none));
partsList.leftBracket(node.leftParenthesis);

// The initializer clause.
if (initializer != null) {
partsList.addCommentsBefore(initializer.beginToken);
partsList.add(buildPiece((b) {
b.visit(initializer);
b.token(forParts.leftSeparator);
}));
} else {
// No initializer, so look at the comments before `;`.
partsList.addCommentsBefore(forParts.leftSeparator);
partsList.add(tokenPiece(forParts.leftSeparator));
}

// The condition clause.
if (forParts.condition case var conditionExpression?) {
partsList.addCommentsBefore(conditionExpression.beginToken);
partsList.add(buildPiece((b) {
b.visit(conditionExpression);
b.token(forParts.rightSeparator);
}));
} else {
partsList.addCommentsBefore(forParts.rightSeparator);
partsList.add(tokenPiece(forParts.rightSeparator));
}

// The update clauses.
if (forParts.updaters.isNotEmpty) {
partsList.addCommentsBefore(forParts.updaters.first.beginToken);
partsList.add(createList(forParts.updaters,
style: const ListStyle(commas: Commas.nonTrailing)));
}
var forPartsPiece = createForLoopParts(
node.leftParenthesis, node.forLoopParts, node.rightParenthesis);
var body = nodePiece(node.body);

partsList.rightBracket(node.rightParenthesis);
forPartsPiece = partsList.build();
var forPiece = ForPiece(forKeyword, forPartsPiece, body,
hasBlockBody: node.body.isSpreadCollection);

case ForPartsWithPattern():
throw UnimplementedError();
// 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);
}

case ForEachParts forEachParts &&
ForEachPartsWithDeclaration(loopVariable: AstNode variable):
case ForEachParts forEachParts &&
ForEachPartsWithIdentifier(identifier: AstNode variable):
// If a for-in loop, treat the for parts like an assignment, so they
// split like:
//
// for (var variable in [
// initializer,
// ]) {
// body;
// }
forPartsPiece = buildPiece((b) {
b.token(node.leftParenthesis);
b.add(createAssignment(
variable, forEachParts.inKeyword, forEachParts.iterable,
splitBeforeOperator: true));
b.token(node.rightParenthesis);
});
return forPiece;
}

case ForEachPartsWithPattern():
throw UnimplementedError();
}
@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,
Expand Down Expand Up @@ -1824,7 +1743,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
var initializerPiece = nodePiece(initializer, commaAfter: true);

variables.add(AssignPiece(variablePiece, initializerPiece,
isValueDelimited: initializer.canBlockSplit));
allowInnerSplit: initializer.canBlockSplit));
} else {
variables.add(tokenPiece(variable.name, commaAfter: true));
}
Expand Down
149 changes: 146 additions & 3 deletions lib/src/front_end/piece_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,137 @@ mixin PieceFactory {
});
}

/// Creates a piece for the parentheses and inner parts of a for statement or
/// for element.
Piece createForLoopParts(Token leftParenthesis, ForLoopParts forLoopParts,
Token rightParenthesis) {
switch (forLoopParts) {
// Edge case: A totally empty for loop is formatted just as `(;;)` with
// no splits or spaces anywhere.
case ForPartsWithExpression(
initialization: null,
leftSeparator: Token(precedingComments: null),
condition: null,
rightSeparator: Token(precedingComments: null),
updaters: NodeList(isEmpty: true),
)
when rightParenthesis.precedingComments == null:
return buildPiece((b) {
b.token(leftParenthesis);
b.token(forLoopParts.leftSeparator);
b.token(forLoopParts.rightSeparator);
b.token(rightParenthesis);
});

case ForParts forParts &&
ForPartsWithDeclarations(variables: AstNode? initializer):
case ForParts forParts &&
ForPartsWithExpression(initialization: AstNode? initializer):
// In a C-style for loop, treat the for loop parts like an argument list
// where each clause is a separate argument. This means that when they
// split, they split like:
//
// for (
// initializerClause;
// conditionClause;
// incrementClause
// ) {
// body;
// }
var partsList =
DelimitedListBuilder(this, const ListStyle(commas: Commas.none));
partsList.leftBracket(leftParenthesis);

// The initializer clause.
if (initializer != null) {
partsList.addCommentsBefore(initializer.beginToken);
partsList.add(buildPiece((b) {
b.visit(initializer);
b.token(forParts.leftSeparator);
}));
} else {
// No initializer, so look at the comments before `;`.
partsList.addCommentsBefore(forParts.leftSeparator);
partsList.add(tokenPiece(forParts.leftSeparator));
}

// The condition clause.
if (forParts.condition case var conditionExpression?) {
partsList.addCommentsBefore(conditionExpression.beginToken);
partsList.add(buildPiece((b) {
b.visit(conditionExpression);
b.token(forParts.rightSeparator);
}));
} else {
partsList.addCommentsBefore(forParts.rightSeparator);
partsList.add(tokenPiece(forParts.rightSeparator));
}

// The update clauses.
if (forParts.updaters.isNotEmpty) {
partsList.addCommentsBefore(forParts.updaters.first.beginToken);
partsList.add(createList(forParts.updaters,
style: const ListStyle(commas: Commas.nonTrailing)));
}

partsList.rightBracket(rightParenthesis);
return partsList.build();

case ForPartsWithPattern():
throw UnimplementedError();

case ForEachParts forEachParts &&
ForEachPartsWithDeclaration(loopVariable: AstNode variable):
case ForEachParts forEachParts &&
ForEachPartsWithIdentifier(identifier: AstNode variable):
// If a for-in loop, treat the for parts like an assignment, so they
// split like:
//
// for (var variable in [
// initializer,
// ]) {
// body;
// }
// TODO(tall): Passing `allowInnerSplit: true` allows output like:
//
// // 1
// for (variable in longExpression +
// thatWraps) {
// ...
// }
//
// Versus the split in the initializer forcing a split before `in` too:
//
// // 2
// for (variable
// in longExpression +
// thatWraps) {
// ...
// }
//
// This is also allowed:
//
// // 3
// for (variable
// in longExpression + thatWraps) {
// ...
// }
//
// Currently, the formatter prefers 1 over 3. We may want to revisit
// that and prefer 3 instead.
return buildPiece((b) {
b.token(leftParenthesis);
b.add(createAssignment(
variable, forEachParts.inKeyword, forEachParts.iterable,
splitBeforeOperator: true, allowInnerSplit: true));
b.token(rightParenthesis);
});

case ForEachPartsWithPattern():
throw UnimplementedError();
}
}

/// Creates a normal (not function-typed) formal parameter with a name and/or
/// type annotation.
///
Expand Down Expand Up @@ -688,11 +819,23 @@ mixin PieceFactory {
/// If [splitBeforeOperator] is `true`, then puts [operator] at the beginning
/// of the next line when it splits. Otherwise, puts the operator at the end
/// of the preceding line.
///
/// If [allowInnerSplit] is `true`, then a newline inside the target or
/// right-hand side doesn't force splitting at the operator itself.
Piece createAssignment(
AstNode target, Token operator, Expression rightHandSide,
{bool splitBeforeOperator = false,
bool includeComma = false,
bool spaceBeforeOperator = true}) {
bool spaceBeforeOperator = true,
bool allowInnerSplit = false}) {
// If the right-hand side can have block formatting, then a newline in
// it doesn't force the operator to split, as in:
//
// var list = [
// element,
// ];
allowInnerSplit |= rightHandSide.canBlockSplit;

if (splitBeforeOperator) {
var targetPiece = nodePiece(target);

Expand All @@ -703,7 +846,7 @@ mixin PieceFactory {
});

return AssignPiece(targetPiece, initializer,
isValueDelimited: rightHandSide.canBlockSplit);
allowInnerSplit: allowInnerSplit);
} else {
var targetPiece = buildPiece((b) {
b.visit(target);
Expand All @@ -713,7 +856,7 @@ mixin PieceFactory {
var initializer = nodePiece(rightHandSide, commaAfter: includeComma);

return AssignPiece(targetPiece, initializer,
isValueDelimited: rightHandSide.canBlockSplit);
allowInnerSplit: allowInnerSplit);
}
}

Expand Down
16 changes: 8 additions & 8 deletions lib/src/piece/assign.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ class AssignPiece extends Piece {
/// The right-hand side of the operation.
final Piece value;

/// Whether the right-hand side is a delimited expression that should receive
/// block-like formatting.
final bool _isValueDelimited;
/// Whether a newline is allowed in the right-hand side without forcing a
/// split at the assignment operator.
final bool _allowInnerSplit;

AssignPiece(this.target, this.value, {bool isValueDelimited = false})
: _isValueDelimited = isValueDelimited;
AssignPiece(this.target, this.value, {bool allowInnerSplit = false})
: _allowInnerSplit = allowInnerSplit;

// TODO(tall): The old formatter allows the first operand of a split
// conditional expression to be on the same line as the `=`, as in:
Expand Down Expand Up @@ -91,9 +91,9 @@ class AssignPiece extends Piece {

@override
void format(CodeWriter writer, State state) {
// A split in either child piece forces splitting after the "=" unless it's
// a delimited expression.
if (state == State.unsplit && !_isValueDelimited) {
// A split in either child piece forces splitting at assignment operator
// unless specifically allowed.
if (!_allowInnerSplit && state == State.unsplit) {
writer.setAllowNewlines(false);
}

Expand Down
Loading

0 comments on commit f88d826

Please sign in to comment.