Skip to content

Commit 120b752

Browse files
authored
Format enums (#1317)
Format enum declarations with and without members. Enums are a little tricky because they are sort of like lists (i.e. ListPiece) and sort of like type declarations (i.e. BlockPiece). Enums without members should be formatted pretty much like any other delimited list where the declaration can collapse to one line (and discard any trailing comma) if it fits or expand to one element per line (and add a trailing comma) otherwise. Enums with members are formatted pretty much like blocks where each constant or member is on its own line. There are a few wrinkles around trailing commas and the semicolon between the constants and members: - If the enum has no members, any trailing `;` is discarded. It doesn't do anything useful and I consider this morally equivalent to discarding other trailing commas. - If the enum has members, then a trailing comma after the last constant is redundant with the subsequent `;`. In that case, it discards the comma. In both of these cases, we have to make sure we don't accidentally lose any comments around that discarded punctuation. So there are a lot of tests for all of those edge cases even though in practice no one writes code like that. Discarding `;` on enums without members means that we treat `;` like whitespace, so I also loosened equalIgnoringWhitespace() to ignore semicolons too. Since enums with and without members are formatted using different codepaths, there are a lot of mostly redundant-seeming tests for them.
1 parent 78b7695 commit 120b752

11 files changed

+761
-30
lines changed

lib/src/ast_extensions.dart

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

77
extension AstNodeExtensions on AstNode {
8+
/// The first token at the beginning of this AST node, not including any
9+
/// tokens for leading doc comments.
10+
///
11+
/// If [node] is an [AnnotatedNode], then [beginToken] includes the
12+
/// leading doc comment, which we want to handle separately. So, in that
13+
/// case, explicitly skip past the doc comment to the subsequent metadata
14+
/// (if there is any), or the beginning of the code.
15+
Token get firstNonCommentToken {
16+
return switch (this) {
17+
AnnotatedNode(metadata: [var annotation, ...]) => annotation.beginToken,
18+
AnnotatedNode(firstTokenAfterCommentAndMetadata: var token) => token,
19+
_ => beginToken
20+
};
21+
}
22+
823
/// The comma token immediately following this if there is one, or `null`.
924
Token? get commaAfter {
1025
var next = endToken.next!;

lib/src/front_end/ast_node_visitor.dart

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,11 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
114114
}
115115

116116
@override
117-
void visitArgumentList(ArgumentList node, {bool nestExpression = true}) {
118-
throw UnimplementedError();
117+
void visitArgumentList(ArgumentList node) {
118+
createList(
119+
leftBracket: node.leftParenthesis,
120+
node.arguments,
121+
rightBracket: node.rightParenthesis);
119122
}
120123

121124
@override
@@ -400,12 +403,81 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
400403

401404
@override
402405
void visitEnumConstantDeclaration(EnumConstantDeclaration node) {
403-
throw UnimplementedError();
406+
token(node.name);
407+
if (node.arguments case var arguments?) {
408+
visit(arguments.typeArguments);
409+
visit(arguments.argumentList);
410+
}
404411
}
405412

406413
@override
407414
void visitEnumDeclaration(EnumDeclaration node) {
408-
throw UnimplementedError();
415+
if (node.metadata.isNotEmpty) throw UnimplementedError();
416+
417+
token(node.enumKeyword);
418+
space();
419+
token(node.name);
420+
visit(node.typeParameters);
421+
space();
422+
423+
if (node.members.isEmpty) {
424+
// If there are no members, format the constants like a delimited list.
425+
// This keeps the enum declaration on one line if it fits.
426+
var builder = DelimitedListBuilder(
427+
this,
428+
const ListStyle(
429+
spaceWhenUnsplit: true, splitListIfBeforeSplits: true));
430+
builder.leftBracket(node.leftBracket);
431+
node.constants.forEach(builder.visit);
432+
433+
builder.rightBracket(semicolon: node.semicolon, node.rightBracket);
434+
pieces.give(builder.build());
435+
} else {
436+
// If there are members, format it like a block where each constant and
437+
// member is on its own line.
438+
token(node.leftBracket);
439+
var leftBracketPiece = pieces.split();
440+
441+
var sequence = SequenceBuilder(this);
442+
for (var constant in node.constants) {
443+
sequence.addCommentsBefore(constant.firstNonCommentToken);
444+
visit(constant);
445+
446+
if (constant != node.constants.last) {
447+
commaAfter(constant);
448+
} else {
449+
// Discard the trailing comma if there is one since there is a
450+
// semicolon to use as the separator, but preserve any comments before
451+
// the discarded comma.
452+
var trailingComma = constant.commaAfter;
453+
if (trailingComma != null) writeCommentsBefore(trailingComma);
454+
455+
token(node.semicolon);
456+
}
457+
458+
sequence.add(pieces.split());
459+
}
460+
461+
// Insert a blank line between the constants and members.
462+
sequence.addBlank();
463+
464+
for (var node in node.members) {
465+
sequence.visit(node);
466+
467+
// If the node has a non-empty braced body, then require a blank line
468+
// between it and the next node.
469+
if (node.hasNonEmptyBody) sequence.addBlank();
470+
}
471+
472+
// Place any comments before the "}" inside the block.
473+
sequence.addCommentsBefore(node.rightBracket);
474+
475+
token(node.rightBracket);
476+
var rightBracketPiece = pieces.take();
477+
478+
pieces.give(
479+
BlockPiece(leftBracketPiece, sequence.build(), rightBracketPiece));
480+
}
409481
}
410482

411483
@override
@@ -757,7 +829,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
757829
visit(constructor.name);
758830
}
759831

760-
finishCall(node.argumentList);
832+
visit(node.argumentList);
761833

762834
// If there was a prefix or constructor name, then make a splittable piece.
763835
if (operations.isNotEmpty) {
@@ -883,7 +955,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
883955

884956
visit(node.methodName);
885957
visit(node.typeArguments);
886-
finishCall(node.argumentList);
958+
visit(node.argumentList);
887959
}
888960

889961
@override

lib/src/front_end/delimited_list_builder.dart

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44
import 'package:analyzer/dart/ast/ast.dart';
55
import 'package:analyzer/dart/ast/token.dart';
6+
import 'package:dart_style/src/ast_extensions.dart';
67

78
import '../comment_type.dart';
89
import '../piece/list.dart';
@@ -75,8 +76,13 @@ class DelimitedListBuilder {
7576
/// ```
7677
///
7778
/// Here, [bracket] will be `)` and [delimiter] will be `}`.
78-
void rightBracket(Token bracket, {Token? delimiter}) {
79+
///
80+
/// If [semicolon] is given, it is the optional `;` in an enum declaration
81+
/// after the enum constants when there are no subsequent members. Comments
82+
/// before the `;` are kept, but the `;` itself is discarded.
83+
void rightBracket(Token bracket, {Token? delimiter, Token? semicolon}) {
7984
// Handle comments after the last element.
85+
var commentsBefore = _visitor.takeCommentsBefore(bracket);
8086

8187
// Merge the comments before the delimiter (if there is one) and the
8288
// bracket. If there is a delimiter, this will move comments between it and
@@ -89,11 +95,16 @@ class DelimitedListBuilder {
8995
// // After:
9096
// f([parameter /* comment */]) {}
9197
// ```
92-
var commentsBefore = _visitor.takeCommentsBefore(bracket);
9398
if (delimiter != null) {
9499
commentsBefore =
95100
_visitor.takeCommentsBefore(delimiter).concatenate(commentsBefore);
96101
}
102+
103+
if (semicolon != null) {
104+
commentsBefore =
105+
_visitor.takeCommentsBefore(semicolon).concatenate(commentsBefore);
106+
}
107+
97108
_addComments(commentsBefore, hasElementAfter: false);
98109

99110
_visitor.token(delimiter);
@@ -123,7 +134,7 @@ class DelimitedListBuilder {
123134
/// Adds [element] to the built list.
124135
void visit(AstNode element) {
125136
// Handle comments between the preceding element and this one.
126-
addCommentsBefore(element.beginToken);
137+
addCommentsBefore(element.firstNonCommentToken);
127138

128139
// Traverse the element itself.
129140
_visitor.visit(element);

lib/src/front_end/piece_factory.dart

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -608,15 +608,6 @@ mixin PieceFactory implements CommentWriter {
608608
isValueDelimited: rightHandSide.isDelimited));
609609
}
610610

611-
/// Writes the argument list part of a constructor, function, or method call
612-
/// after the name has been written.
613-
void finishCall(ArgumentList argumentList) {
614-
createList(
615-
leftBracket: argumentList.leftParenthesis,
616-
argumentList.arguments,
617-
rightBracket: argumentList.rightParenthesis);
618-
}
619-
620611
/// Finishes writing a named function declaration or anonymous function
621612
/// expression after the return type (if any) and name (if any) has been
622613
/// written.

lib/src/front_end/sequence_builder.dart

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44
import 'package:analyzer/dart/ast/ast.dart';
55
import 'package:analyzer/dart/ast/token.dart';
6+
import 'package:dart_style/src/ast_extensions.dart';
67

78
import '../constants.dart';
89
import '../piece/piece.dart';
@@ -46,17 +47,7 @@ class SequenceBuilder {
4647
/// Visits [node] and adds the resulting [Piece] to this sequence, handling
4748
/// any comments or blank lines that appear before it.
4849
void visit(AstNode node, {int? indent}) {
49-
var token = switch (node) {
50-
// If [node] is an [AnnotatedNode], then [beginToken] includes the
51-
// leading doc comment, which we want to handle separately. So, in that
52-
// case, explicitly skip past the doc comment to the subsequent metadata
53-
// (if there is any), or the beginning of the code.
54-
AnnotatedNode(metadata: [var annotation, ...]) => annotation.beginToken,
55-
AnnotatedNode() => node.firstTokenAfterCommentAndMetadata,
56-
_ => node.beginToken
57-
};
58-
59-
addCommentsBefore(token);
50+
addCommentsBefore(node.firstNonCommentToken);
6051
_visitor.visit(node);
6152
add(_visitor.pieces.split(), indent: indent);
6253
}
@@ -96,7 +87,6 @@ class SequenceBuilder {
9687
if (_elements.isNotEmpty && comments.isHanging(i)) {
9788
// Attach the comment to the previous token.
9889
_visitor.space();
99-
10090
_visitor.pieces.writeComment(comment, hanging: true);
10191
} else {
10292
// Write the comment as its own sequence piece.

lib/src/string_compare.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
/// add and remove trailing commas. It treats, `[`, `]`, `{`, and `}` as
2020
/// whitespace characters because the formatter may move a comment if it
2121
/// appears near the closing delimiter of an optional parameter section.
22+
///
23+
/// It treats `;` as whitespace because a trailing `;` inside an enum
24+
/// declaration that has no members will be removed.
2225
bool _isWhitespace(int c) {
2326
// Not using a set or something more elegant because this code is on the hot
2427
// path and this large expression is significantly faster than a set lookup.
@@ -27,6 +30,7 @@ bool _isWhitespace(int c) {
2730
c == 0x005d || // Treat `]` as "whitespace".
2831
c == 0x007b || // Treat `{` as "whitespace".
2932
c == 0x007d || // Treat `}` as "whitespace".
33+
c == 0x003b || // Treat `;` as "whitespace".
3034
c >= 0x0009 && c <= 0x000d || // Control characters.
3135
c == 0x0020 || // SPACE.
3236
c == 0x0085 || // Control characters.

test/declaration/enum.unit

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
40 columns |
2+
>>> Single value.
3+
enum Unity {one}
4+
<<<
5+
enum Unity { one }
6+
>>> Multiple unsplit values.
7+
enum Primate{
8+
9+
bonobo,
10+
11+
12+
13+
chimp,
14+
15+
16+
gorilla
17+
18+
19+
20+
}
21+
<<<
22+
enum Primate { bonobo, chimp, gorilla }
23+
>>> Remove trailing comma if values don't split.
24+
enum Primate{bonobo,chimp,}
25+
<<<
26+
enum Primate { bonobo, chimp }
27+
>>> Split values and insert trailing comma.
28+
enum Primate{bonobo,chimp,gorilla,organutan}
29+
<<<
30+
enum Primate {
31+
bonobo,
32+
chimp,
33+
gorilla,
34+
organutan,
35+
}
36+
>>> Insert blank line before and after enum declaration.
37+
var x = 1;
38+
enum A { a }
39+
var y = 2;
40+
<<<
41+
var x = 1;
42+
43+
enum A { a }
44+
45+
var y = 2;
46+
>>> Remove semicolon if there are no members.
47+
enum E { a, b; }
48+
<<<
49+
enum E { a, b }
50+
>>> Remove trailing comma and semicolon if unsplit.
51+
enum E {a,b,c,;}
52+
<<<
53+
enum E { a, b, c }
54+
>>> Argument lists in values.
55+
enum Args {
56+
first(),second(a,b,c),
57+
third(named:1,2,another:3)
58+
}
59+
<<<
60+
enum Args {
61+
first(),
62+
second(a, b, c),
63+
third(named: 1, 2, another: 3),
64+
}
65+
>>> Split argument lists in values.
66+
enum Args {
67+
firstEnumValue(longArgument,anotherArgument),
68+
secondEnumValue(longArgument,anotherArgument,aThirdArgument,theLastOne),
69+
thirdGenericOne<FirstTypeArgument,
70+
SecondTypeArgument<NestedTypeArgument,AnotherNestedTypeArgument>,
71+
LastTypeArgument>(namedArgument: firstValue,anotherNamed:argumentValue)
72+
}
73+
<<<
74+
enum Args {
75+
firstEnumValue(
76+
longArgument,
77+
anotherArgument,
78+
),
79+
secondEnumValue(
80+
longArgument,
81+
anotherArgument,
82+
aThirdArgument,
83+
theLastOne,
84+
),
85+
thirdGenericOne<
86+
FirstTypeArgument,
87+
SecondTypeArgument<
88+
NestedTypeArgument,
89+
AnotherNestedTypeArgument
90+
>,
91+
LastTypeArgument
92+
>(
93+
namedArgument: firstValue,
94+
anotherNamed: argumentValue,
95+
),
96+
}
97+
>>> Generic enum type.
98+
enum MagicNumbers< T extends num , S> {
99+
one(1), two(2),pi<double,String>(3.14159)
100+
}
101+
<<<
102+
enum MagicNumbers<T extends num, S> {
103+
one(1),
104+
two(2),
105+
pi<double, String>(3.14159),
106+
}
107+
>>> Split in type parameters forces body to split.
108+
enum SomeEnumType<LongTypeParameterName, Another> { a, b, c }
109+
<<<
110+
enum SomeEnumType<
111+
LongTypeParameterName,
112+
Another
113+
> {
114+
a,
115+
b,
116+
c,
117+
}

0 commit comments

Comments
 (0)