Skip to content

Commit d8baf12

Browse files
Format list literals (and some other things). (#1281)
Format list literals (and some other things). I'm trying to keep these PRs mostly well-contained and not just adding a random pile of formatting. But I'm also trying to make sure I don't drop stuff on the floor. For this one, I filled in a few more things that lists touch on: - Handle type argument lists in lists and function calls. - Support block-like formatting for right-hand sides of `=`. - Format parenthesized expressions. - Format named type annotations with type arguments and nullable types. - Add support for states with different costs so we can prioritize splitting choices. Co-authored-by: Nate Bosch <[email protected]>
1 parent c179702 commit d8baf12

17 files changed

+573
-24
lines changed

lib/src/ast_extensions.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,34 @@ extension AstIterableExtensions on Iterable<AstNode> {
9292
}
9393

9494
extension ExpressionExtensions on Expression {
95+
/// Whether this expression is a "delimited" one that allows block-like
96+
/// formatting in some contexts. For example, in an assignment, a split in
97+
/// the assigned value is usually indented:
98+
///
99+
/// ```
100+
/// var variableName =
101+
/// longValue;
102+
/// ```
103+
///
104+
/// But not if the initializer is a delimited expression and we don't split
105+
/// at the `=`:
106+
///
107+
/// ```
108+
/// var variableName = [
109+
/// element,
110+
/// ];
111+
/// ```
112+
bool get isDelimited => switch (this) {
113+
ParenthesizedExpression(:var expression) => expression.isDelimited,
114+
ListLiteral() => true,
115+
MethodInvocation() => true,
116+
// TODO(tall): Map and set literals.
117+
// TODO(tall): Record literals.
118+
// TODO(tall): Instance creation expressions (`new` and `const`).
119+
// TODO(tall): Switch expressions.
120+
_ => false,
121+
};
122+
95123
/// Whether this is an argument in an argument list with a trailing comma.
96124
bool get isTrailingCommaArgument {
97125
var parent = this.parent;

lib/src/back_end/code_writer.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,7 @@ class CodeWriter {
174174

175175
var state = _pieceStates.pieceState(piece);
176176

177-
// TODO(tall): Support pieces with different split costs, and possibly
178-
// different costs for each state value.
179-
if (state != State.initial) _cost++;
177+
_cost += state.cost;
180178

181179
// TODO(perf): Memoize this. Might want to create a nested PieceWriter
182180
// instead of passing in `this` so we can better control what state needs

lib/src/front_end/ast_node_visitor.dart

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,28 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
526526

527527
@override
528528
void visitListLiteral(ListLiteral node) {
529-
throw UnimplementedError();
529+
visit(node.typeArguments);
530+
531+
var builder = DelimitedListBuilder(this);
532+
builder.leftBracket(node.leftBracket);
533+
534+
// TODO(tall): Support a line comment inside a list literal as a signal to
535+
// preserve internal newlines. So if you have:
536+
//
537+
// ```
538+
// var list = [
539+
// 1, 2, 3, // comment
540+
// 4, 5, 6,
541+
// ];
542+
// ```
543+
//
544+
// The formatter will preserve the newline after element 3 and the lack of
545+
// them after the other elements.
546+
547+
node.elements.forEach(builder.add);
548+
549+
builder.rightBracket(node.rightBracket);
550+
writer.push(builder.build());
530551
}
531552

532553
@override
@@ -570,9 +591,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
570591
if (node.target != null) throw UnimplementedError();
571592

572593
visit(node.methodName);
573-
574-
// TODO(tall): Support type arguments to method calls.
575-
if (node.typeArguments != null) throw UnimplementedError();
594+
visit(node.typeArguments);
576595

577596
var builder = DelimitedListBuilder(this);
578597
builder.leftBracket(node.argumentList.leftParenthesis);
@@ -603,9 +622,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
603622
if (node.importPrefix != null) throw UnimplementedError();
604623

605624
token(node.name2);
606-
607-
// TODO(tall): Handle type arguments.
608-
if (node.typeArguments != null) throw UnimplementedError();
625+
visit(node.typeArguments);
609626

610627
// TODO(tall): Handle nullable types.
611628
if (node.question != null) throw UnimplementedError();
@@ -648,7 +665,9 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
648665

649666
@override
650667
void visitParenthesizedExpression(ParenthesizedExpression node) {
651-
throw UnimplementedError();
668+
token(node.leftParenthesis);
669+
visit(node.expression);
670+
token(node.rightParenthesis);
652671
}
653672

654673
@override
@@ -877,7 +896,15 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
877896

878897
@override
879898
void visitTypeArgumentList(TypeArgumentList node) {
880-
throw UnimplementedError();
899+
var builder = DelimitedListBuilder(this);
900+
builder.leftBracket(node.leftBracket);
901+
902+
for (var arguments in node.arguments) {
903+
builder.add(arguments);
904+
}
905+
906+
builder.rightBracket(node.rightBracket);
907+
writer.push(builder.build());
881908
}
882909

883910
@override

lib/src/front_end/delimited_list_builder.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,25 @@ class DelimitedListBuilder {
3131

3232
late final Piece _rightBracket;
3333

34+
bool _trailingComma = true;
35+
3436
DelimitedListBuilder(this._visitor);
3537

3638
/// The list of comments following the most recently written element before
3739
/// any comma following the element.
3840
CommentSequence _commentsBeforeComma = CommentSequence.empty;
3941

40-
ListPiece build() =>
41-
ListPiece(_leftBracket, _elements, _blanksAfter, _rightBracket);
42+
ListPiece build() => ListPiece(
43+
_leftBracket, _elements, _blanksAfter, _rightBracket, _trailingComma);
4244

4345
/// Adds the opening [bracket] to the built list.
4446
void leftBracket(Token bracket) {
4547
_visitor.token(bracket);
4648
_leftBracket = _visitor.writer.pop();
4749
_visitor.writer.split();
50+
51+
// No trailing commas in type argument and type parameter lists.
52+
if (bracket.type == TokenType.LT) _trailingComma = false;
4853
}
4954

5055
/// Adds the closing [bracket] to the built list along with any comments that

lib/src/front_end/piece_factory.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:analyzer/dart/ast/ast.dart';
55
import 'package:analyzer/dart/ast/token.dart';
66

77
import '../ast_extensions.dart';
8+
import '../piece/assign.dart';
89
import '../piece/block.dart';
910
import '../piece/import.dart';
1011
import '../piece/infix.dart';
@@ -245,13 +246,14 @@ mixin PieceFactory implements CommentWriter {
245246

246247
writer.space();
247248
token(equalsOperator);
248-
var equals = writer.pop();
249+
var target = writer.pop();
249250
writer.split();
250251

251252
visit(rightHandSide);
252253

253254
var initializer = writer.pop();
254-
writer.push(InfixPiece([equals, initializer]));
255+
writer.push(AssignPiece(target, initializer,
256+
isValueDelimited: rightHandSide!.isDelimited));
255257
}
256258

257259
/// Writes an optional modifier that precedes other code.

lib/src/piece/assign.dart

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
import '../back_end/code_writer.dart';
5+
import '../constants.dart';
6+
import 'piece.dart';
7+
8+
/// A piece for any construct where `=` is followed by an expression: variable
9+
/// initializer, assignment, constructor initializer, etc. Assignments can be
10+
/// formatted three ways:
11+
///
12+
/// [State.initial] No split at all:
13+
///
14+
/// ```
15+
/// var x = 123;
16+
/// ```
17+
///
18+
/// [_insideValue] If the value is a delimited "block-like" expression,
19+
/// then we can split inside the block but not at the `=` with no additional
20+
/// indentation:
21+
///
22+
/// ```
23+
/// var list = [
24+
/// element,
25+
/// ];
26+
/// ```
27+
///
28+
/// [State.split] Split after the `=`:
29+
///
30+
/// ```
31+
/// var name =
32+
/// longValueExpression;
33+
/// ```
34+
class AssignPiece extends Piece {
35+
/// Split inside the value but not at the `=`.
36+
///
37+
/// This is only allowed when the value is a delimited expression.
38+
static const State _insideValue = State(1);
39+
40+
/// Split after the `=` and allow splitting inside the value.
41+
///
42+
/// This is more costly because, when it's possible to split inside a
43+
/// delimited value, we want to prefer that.
44+
static const State _atEquals = State(2, cost: 2);
45+
46+
/// The left-hand side of the `=` and the `=` itself.
47+
final Piece target;
48+
49+
/// The right-hand side of the `=`.
50+
final Piece value;
51+
52+
final bool _isValueDelimited;
53+
54+
AssignPiece(this.target, this.value, {required bool isValueDelimited})
55+
: _isValueDelimited = isValueDelimited;
56+
57+
@override
58+
List<State> get states => [if (_isValueDelimited) _insideValue, _atEquals];
59+
60+
@override
61+
void format(CodeWriter writer, State state) {
62+
writer.format(target);
63+
64+
// A split inside the value forces splitting at the "=" unless it's a
65+
// delimited expression.
66+
if (state == State.initial) writer.setAllowNewlines(false);
67+
68+
// Don't indent a split delimited expression.
69+
if (state != _insideValue) writer.setIndent(Indent.expression);
70+
71+
writer.splitIf(state == _atEquals);
72+
writer.format(value);
73+
}
74+
75+
@override
76+
void forEachChild(void Function(Piece piece) callback) {
77+
callback(target);
78+
callback(value);
79+
}
80+
81+
@override
82+
String toString() => 'Assign';
83+
}

lib/src/piece/list.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ class ListPiece extends Piece {
3232
/// The ")" after the arguments.
3333
final Piece _after;
3434

35-
ListPiece(this._before, this._arguments, this._blanksAfter, this._after);
35+
/// Whether a split list should get a trailing comma.
36+
///
37+
/// This is true in most constructs in Dart, but trailing commas are
38+
/// disallowed by the language in type argument and type parameter lists.
39+
final bool _trailingComma;
40+
41+
ListPiece(this._before, this._arguments, this._blanksAfter, this._after,
42+
this._trailingComma);
3643

3744
/// Don't let the list split if there is nothing in it.
3845
@override
@@ -67,7 +74,8 @@ class ListPiece extends Piece {
6774
writer.newline();
6875
for (var i = 0; i < _arguments.length; i++) {
6976
var argument = _arguments[i];
70-
argument.format(writer, omitComma: false);
77+
argument.format(writer,
78+
omitComma: i == _arguments.length - 1 && !_trailingComma);
7179
if (i < _arguments.length - 1) {
7280
writer.newline(blank: _blanksAfter.contains(argument));
7381
}
@@ -76,6 +84,7 @@ class ListPiece extends Piece {
7684
writer.newline();
7785
}
7886

87+
writer.setAllowNewlines(true);
7988
writer.format(_after);
8089
}
8190

lib/src/piece/piece.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class TextPiece extends Piece {
9696
/// Each state identifies one way that a piece can be split into multiple lines.
9797
/// Each piece determines how its states are interpreted.
9898
class State implements Comparable<State> {
99-
static const initial = State(0);
99+
static const initial = State(0, cost: 0);
100100

101101
/// The maximally split state a piece can be in.
102102
///
@@ -106,7 +106,10 @@ class State implements Comparable<State> {
106106

107107
final int _value;
108108

109-
const State(this._value);
109+
/// How much a solution is penalized when this state is chosen.
110+
final int cost;
111+
112+
const State(this._value, {this.cost = 1});
110113

111114
@override
112115
int compareTo(State other) => _value.compareTo(other._value);

lib/src/piece/variable.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class VariablePiece extends Piece {
3939
static const State _betweenVariables = State(1);
4040

4141
/// Split after the type annotation and between each variable.
42-
static const State _afterType = State(2);
42+
static const State _afterType = State(2, cost: 2);
4343

4444
/// The leading keywords (`var`, `final`, `late`) and optional type
4545
/// annotation.

test/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ invocation/ - Test formatting function and member invocations.
6868
member/ - Test formatting class/enum/extension/mixin member declarations.
6969
statement/ - Test formatting statements.
7070
top_level/ - Test formatting top-level declarations and directives.
71+
type/ - Test formatting type annotations.
7172
```
7273

7374
These tests are all run by `tall_format_test.dart`.

0 commit comments

Comments
 (0)