Skip to content

Commit 04883d6

Browse files
authored
Better indentation for property access as binary operator operand. (#1615)
Better indentation for property access as binary operator operand. While I was at it, I figured out a more systematic way to hoist leading comments out from the subexpressions containing the token that the comments precede. That fixes this bug and also lets us get rid of many of the one-off places where we were manually hoisting comments out of some subexpressions. Surprisingly, even though this touches visitNode() which is a fairly hot method, it doesn't seem to measurably harm performance.
1 parent e3aaaf2 commit 04883d6

File tree

7 files changed

+188
-98
lines changed

7 files changed

+188
-98
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
## 3.0.1-wip
22

3-
* Ensure comment formatting is idempotent (#1606).
43
* Handle comments and metadata before variables more gracefully (#1604).
4+
* Ensure comment formatting is idempotent (#1606).
5+
* Better indentation of leading comments on property accesses in binary operator
6+
operands (#1611).
57

68
## 3.0.0
79

lib/src/front_end/ast_node_visitor.dart

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import '../piece/case.dart';
1414
import '../piece/constructor.dart';
1515
import '../piece/control_flow.dart';
1616
import '../piece/infix.dart';
17+
import '../piece/leading_comment.dart';
1718
import '../piece/list.dart';
1819
import '../piece/piece.dart';
1920
import '../piece/type.dart';
@@ -329,10 +330,6 @@ final class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {
329330

330331
@override
331332
void visitConditionalExpression(ConditionalExpression node) {
332-
// Hoist any comments before the condition operand so they don't force the
333-
// conditional expression to split.
334-
var leadingComments = pieces.takeCommentsBefore(node.firstNonCommentToken);
335-
336333
// Flatten a series of else-if-like chained conditionals into a single long
337334
// infix piece. This produces a flattened style like:
338335
//
@@ -380,7 +377,7 @@ final class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {
380377
piece.pin(State.split);
381378
}
382379

383-
pieces.add(prependLeadingComments(leadingComments, piece));
380+
pieces.add(piece);
384381
}
385382

386383
@override
@@ -390,7 +387,17 @@ final class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {
390387
pieces.token(node.leftParenthesis);
391388

392389
if (node.equalToken case var equals?) {
393-
writeInfix(node.name, equals, node.value!, hanging: true);
390+
// Hoist comments so that they don't force the `==` to split.
391+
pieces.hoistLeadingComments(node.name.firstNonCommentToken, () {
392+
return InfixPiece([
393+
pieces.build(() {
394+
pieces.visit(node.name);
395+
pieces.space();
396+
pieces.token(equals);
397+
}),
398+
nodePiece(node.value!)
399+
]);
400+
});
394401
} else {
395402
pieces.visit(node.name);
396403
}
@@ -1966,7 +1973,46 @@ final class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {
19661973
var previousContext = _parentContext;
19671974
_parentContext = context;
19681975

1969-
node.accept(this);
1976+
// If there are comments before this node, then some of them may be leading
1977+
// comments. If so, capture them now. We do this here so that the comments
1978+
// are wrapped around the outermost possible Piece for the AST node. For
1979+
// example:
1980+
//
1981+
// // Comment
1982+
// a.b && c || d ? e : f;
1983+
//
1984+
// Here, the node that actually owns the token before the comment is `a`,
1985+
// which is an identifier expression inside a property access inside an
1986+
// `&&` expression inside an `||` expression inside `?:` expression. If we
1987+
// attach the comment to the identifier expression, then the newline from
1988+
// the comment will force all of those surrounding pieces to split:
1989+
//
1990+
// // Comment
1991+
// a
1992+
// .b &&
1993+
// c ||
1994+
// d
1995+
// ? e
1996+
// : f;
1997+
//
1998+
// Instead, we hoist the comment out of all of those and then have comment
1999+
// precede them all so that they don't split.
2000+
var firstToken = node.firstNonCommentToken;
2001+
if (firstToken.precedingComments != null) {
2002+
var comments = pieces.takeCommentsBefore(firstToken);
2003+
var piece = pieces.build(() {
2004+
node.accept(this);
2005+
});
2006+
2007+
// Check again because the preceding comments may not necessarily end up
2008+
// as leading comments.
2009+
if (comments.isNotEmpty) piece = LeadingCommentPiece(comments, piece);
2010+
2011+
pieces.add(piece);
2012+
} else {
2013+
// No preceding comments, so just visit it inline.
2014+
node.accept(this);
2015+
}
19702016

19712017
_parentContext = previousContext;
19722018
}

lib/src/front_end/chain_builder.dart

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:analyzer/dart/ast/token.dart';
88
import '../ast_extensions.dart';
99
import '../constants.dart';
1010
import '../piece/chain.dart';
11+
import '../piece/leading_comment.dart';
1112
import '../piece/list.dart';
1213
import '../piece/piece.dart';
1314
import 'piece_factory.dart';
@@ -75,18 +76,7 @@ final class ChainBuilder {
7576

7677
// When [_root] is a cascade, the chain is the series of cascade sections.
7778
for (var section in cascade.cascadeSections) {
78-
// Hoist any leading comment out so that if the cascade section is a
79-
// setter, we don't unnecessarily split at the `=` like:
80-
//
81-
// target
82-
// // comment
83-
// ..setter =
84-
// value;
85-
var leadingComments =
86-
_visitor.pieces.takeCommentsBefore(section.firstNonCommentToken);
87-
88-
var piece = _visitor.prependLeadingComments(
89-
leadingComments, _visitor.nodePiece(section));
79+
var piece = _visitor.nodePiece(section);
9080

9181
var callType = switch (section) {
9282
// Force the cascade to split if there are leading comments before
@@ -96,7 +86,7 @@ final class ChainBuilder {
9686
// ..method(
9787
// argument,
9888
// );
99-
_ when leadingComments.isNotEmpty => CallType.unsplittableCall,
89+
_ when piece is LeadingCommentPiece => CallType.unsplittableCall,
10090

10191
// If the section is itself a method chain, then force the cascade to
10292
// split if the method does, as in:

lib/src/front_end/piece_factory.dart

Lines changed: 45 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import '../piece/control_flow.dart';
1212
import '../piece/for.dart';
1313
import '../piece/if_case.dart';
1414
import '../piece/infix.dart';
15-
import '../piece/leading_comment.dart';
1615
import '../piece/list.dart';
1716
import '../piece/piece.dart';
1817
import '../piece/sequence.dart';
@@ -285,15 +284,6 @@ mixin PieceFactory {
285284
return builder.build();
286285
}
287286

288-
/// If [leadingComments] is not empty, returns [piece] wrapped in a
289-
/// [LeadingCommentPiece] that prepends them.
290-
///
291-
/// Otherwise, just returns [piece].
292-
Piece prependLeadingComments(List<Piece> leadingComments, Piece piece) {
293-
if (leadingComments.isEmpty) return piece;
294-
return LeadingCommentPiece(leadingComments, piece);
295-
}
296-
297287
/// Writes the leading keyword and parenthesized expression at the beginning
298288
/// of an `if`, `while`, or `switch` expression or statement.
299289
void writeControlFlowStart(Token keyword, Token leftParenthesis,
@@ -500,21 +490,18 @@ mixin PieceFactory {
500490

501491
// Hoist any leading comments so they don't force the for-in clauses
502492
// to split.
503-
var leadingComments = const <Piece>[];
504-
if (metadata.isEmpty) {
505-
leadingComments = pieces.takeCommentsBefore(keyword);
506-
}
507-
508-
// Use a nested piece so that the metadata precedes the keyword and
509-
// not the `(`.
510-
var forInPiece =
511-
pieces.build(metadata: metadata, inlineMetadata: true, () {
512-
pieces.token(keyword);
513-
pieces.space();
514-
_writeForIn(pattern, forEachParts.inKeyword, forEachParts.iterable);
493+
pieces.hoistLeadingComments(
494+
metadata.firstOrNull?.beginToken ?? keyword, () {
495+
// Use a nested piece so that the metadata precedes the keyword and
496+
// not the `(`.
497+
return pieces.build(metadata: metadata, inlineMetadata: true, () {
498+
pieces.token(keyword);
499+
pieces.space();
500+
_writeForIn(
501+
pattern, forEachParts.inKeyword, forEachParts.iterable);
502+
});
515503
});
516504

517-
pieces.add(prependLeadingComments(leadingComments, forInPiece));
518505
pieces.token(rightParenthesis);
519506
});
520507
}
@@ -645,22 +632,21 @@ mixin PieceFactory {
645632
// int f() {}
646633
var firstToken =
647634
modifiers.nonNulls.firstOrNull ?? returnType.firstNonCommentToken;
648-
var leadingComments = pieces.takeCommentsBefore(firstToken);
635+
pieces.hoistLeadingComments(firstToken, () {
636+
var returnTypePiece = pieces.build(() {
637+
for (var keyword in modifiers) {
638+
pieces.modifier(keyword);
639+
}
649640

650-
var returnTypePiece = pieces.build(() {
651-
for (var keyword in modifiers) {
652-
pieces.modifier(keyword);
653-
}
641+
pieces.visit(returnType);
642+
});
654643

655-
pieces.visit(returnType);
656-
});
644+
var signature = pieces.build(() {
645+
writeFunction();
646+
});
657647

658-
var signature = pieces.build(() {
659-
writeFunction();
648+
return VariablePiece(returnTypePiece, [signature], hasType: true);
660649
});
661-
662-
pieces.add(prependLeadingComments(leadingComments,
663-
VariablePiece(returnTypePiece, [signature], hasType: true)));
664650
}
665651

666652
/// If [parameter] has a [defaultValue] then writes a piece for the parameter
@@ -941,10 +927,6 @@ mixin PieceFactory {
941927
/// separate tokens, as in `foo is! Bar`.
942928
void writeInfix(AstNode left, Token operator, AstNode right,
943929
{bool hanging = false, Token? operator2}) {
944-
// Hoist any comments before the first operand so they don't force the
945-
// infix operator to split.
946-
var leadingComments = pieces.takeCommentsBefore(left.firstNonCommentToken);
947-
948930
var leftPiece = pieces.build(() {
949931
pieces.visit(left);
950932
if (hanging) {
@@ -964,8 +946,7 @@ mixin PieceFactory {
964946
pieces.visit(right);
965947
});
966948

967-
pieces.add(prependLeadingComments(
968-
leadingComments, InfixPiece([leftPiece, rightPiece])));
949+
pieces.add(InfixPiece([leftPiece, rightPiece]));
969950
}
970951

971952
/// Writes a chained infix operation: a binary operator expression, or
@@ -986,10 +967,6 @@ mixin PieceFactory {
986967
void writeInfixChain<T extends AstNode>(
987968
T node, BinaryOperation Function(T node) destructure,
988969
{int? precedence, bool indent = true}) {
989-
// Hoist any comments before the first operand so they don't force the
990-
// infix operator to split.
991-
var leadingComments = pieces.takeCommentsBefore(node.firstNonCommentToken);
992-
993970
var operands = <Piece>[];
994971

995972
void traverse(AstNode e) {
@@ -1017,8 +994,7 @@ mixin PieceFactory {
1017994
traverse(node);
1018995
}));
1019996

1020-
pieces.add(prependLeadingComments(
1021-
leadingComments, InfixPiece(operands, indent: indent)));
997+
pieces.add(InfixPiece(operands, indent: indent));
1022998
}
1023999

10241000
/// Writes a [ListPiece] for the given bracket-delimited set of elements.
@@ -1421,17 +1397,14 @@ mixin PieceFactory {
14211397
void _writeForIn(AstNode leftHandSide, Token inKeyword, Expression sequence) {
14221398
// Hoist any leading comments so they don't force the for-in clauses to
14231399
// split.
1424-
var leadingComments =
1425-
pieces.takeCommentsBefore(leftHandSide.firstNonCommentToken);
1426-
1427-
var leftPiece =
1428-
nodePiece(leftHandSide, context: NodeContext.forLoopVariable);
1429-
var sequencePiece = _createForInSequence(inKeyword, sequence);
1400+
pieces.hoistLeadingComments(leftHandSide.firstNonCommentToken, () {
1401+
var leftPiece =
1402+
nodePiece(leftHandSide, context: NodeContext.forLoopVariable);
1403+
var sequencePiece = _createForInSequence(inKeyword, sequence);
14301404

1431-
pieces.add(prependLeadingComments(
1432-
leadingComments,
1433-
ForInPiece(leftPiece, sequencePiece,
1434-
canBlockSplitSequence: sequence.canBlockSplit)));
1405+
return ForInPiece(leftPiece, sequencePiece,
1406+
canBlockSplitSequence: sequence.canBlockSplit);
1407+
});
14351408
}
14361409

14371410
/// Writes the `<variable> in <expression>` part of a for-in loop when the
@@ -1444,28 +1417,24 @@ mixin PieceFactory {
14441417
DeclaredIdentifier identifier, Token inKeyword, Expression sequence) {
14451418
// Hoist any leading comments so they don't force the for-in clauses
14461419
// to split.
1447-
var leadingComments = const <Piece>[];
1448-
if (identifier.metadata.isEmpty) {
1449-
leadingComments = pieces
1450-
.takeCommentsBefore(identifier.firstTokenAfterCommentAndMetadata);
1451-
}
1452-
1453-
// Use a nested piece so that the metadata precedes the keyword and
1454-
// not the `(`.
1455-
var forInPiece =
1456-
pieces.build(metadata: identifier.metadata, inlineMetadata: true, () {
1457-
var leftPiece = pieces.build(() {
1458-
writeParameter(
1459-
modifiers: [identifier.keyword], identifier.type, identifier.name);
1460-
});
1420+
pieces.hoistLeadingComments(identifier.beginToken, () {
1421+
// Use a nested piece so that the metadata precedes the keyword and
1422+
// not the `(`.
1423+
return pieces.build(metadata: identifier.metadata, inlineMetadata: true,
1424+
() {
1425+
var leftPiece = pieces.build(() {
1426+
writeParameter(
1427+
modifiers: [identifier.keyword],
1428+
identifier.type,
1429+
identifier.name);
1430+
});
14611431

1462-
var sequencePiece = _createForInSequence(inKeyword, sequence);
1432+
var sequencePiece = _createForInSequence(inKeyword, sequence);
14631433

1464-
pieces.add(ForInPiece(leftPiece, sequencePiece,
1465-
canBlockSplitSequence: sequence.canBlockSplit));
1434+
pieces.add(ForInPiece(leftPiece, sequencePiece,
1435+
canBlockSplitSequence: sequence.canBlockSplit));
1436+
});
14661437
});
1467-
1468-
pieces.add(prependLeadingComments(leadingComments, forInPiece));
14691438
}
14701439

14711440
/// Creates a piece for the `in <sequence>` part of a for-in loop.

lib/src/front_end/piece_writer.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import '../back_end/solver.dart';
1010
import '../dart_formatter.dart';
1111
import '../debug.dart' as debug;
1212
import '../piece/adjacent.dart';
13+
import '../piece/leading_comment.dart';
1314
import '../piece/list.dart';
1415
import '../piece/piece.dart';
1516
import '../piece/text.dart';
@@ -302,6 +303,28 @@ final class PieceWriter {
302303
return _splitComments(_comments.takeCommentsBefore(token), token);
303304
}
304305

306+
/// Takes any comments preceding [firstToken] and wraps them around the piece
307+
/// generated by [buildCallback].
308+
///
309+
/// For comments preceding a single AST node, this hoisting is handled
310+
/// automatically. But there are some places in the language where there is
311+
/// a conceptual piece of syntax that we want to hoist the comments out of
312+
/// so that the syntax doesn't split but there isn't actually a single AST
313+
/// node associated with that syntax.
314+
///
315+
/// This method lets you hoist comments before an arbitary amount of syntax
316+
/// visited and built by calling [buildCallback].
317+
void hoistLeadingComments(Token firstToken, Piece Function() buildCallback) {
318+
var leadingComments = takeCommentsBefore(firstToken);
319+
320+
var piece = buildCallback();
321+
if (leadingComments.isNotEmpty) {
322+
piece = LeadingCommentPiece(leadingComments, piece);
323+
}
324+
325+
add(piece);
326+
}
327+
305328
/// Begins a new [CodeToken] that can potentially have more code written to
306329
/// it.
307330
void _beginCodeToken(Token token) {

0 commit comments

Comments
 (0)