Skip to content

Commit d55da0b

Browse files
authored
Format class declarations. (#1309)
Format class declarations. This handles all of the stuff that can appear outside of the braces in a class declaration: - The `class` keyword and any class modifiers. - The name. - Type parameters, if any. - The `extends`, `with`, and/or `implements` clauses. - The `native` clause. It doesn't handle members yet. It also handles ensuring there is a blank line before and after class declarations. In the process of doing that, I also migrated over similar code for function declarations where we ensure there is a blank line after non-empty functions.
1 parent d4db548 commit d55da0b

16 files changed

+738
-23
lines changed

lib/src/ast_extensions.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ extension AstNodeExtensions on AstNode {
3333
body = node.body;
3434
} else if (node is FunctionDeclarationStatement) {
3535
body = node.functionDeclaration.functionExpression.body;
36+
} else if (node is FunctionDeclaration) {
37+
body = node.functionExpression.body;
3638
}
3739

3840
return body is BlockFunctionBody && body.block.statements.isNotEmpty;

lib/src/front_end/ast_node_visitor.dart

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:analyzer/dart/ast/token.dart';
66
import 'package:analyzer/dart/ast/visitor.dart';
77
import 'package:analyzer/source/line_info.dart';
88

9+
import '../ast_extensions.dart';
910
import '../constants.dart';
1011
import '../dart_formatter.dart';
1112
import '../piece/block.dart';
@@ -76,7 +77,17 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
7677
}
7778

7879
for (var declaration in node.declarations) {
80+
var hasBody = declaration is ClassDeclaration ||
81+
declaration is EnumDeclaration ||
82+
declaration is ExtensionDeclaration;
83+
84+
// Add a blank line before types with bodies.
85+
if (hasBody) sequence.addBlank();
86+
7987
sequence.visit(declaration);
88+
89+
// Add a blank line after type or function declarations with bodies.
90+
if (hasBody || declaration.hasNonEmptyBody) sequence.addBlank();
8091
}
8192
} else {
8293
// Just formatting a single statement.
@@ -194,7 +205,26 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
194205

195206
@override
196207
void visitClassDeclaration(ClassDeclaration node) {
197-
throw UnimplementedError();
208+
createType(
209+
node.metadata,
210+
[
211+
node.abstractKeyword,
212+
node.baseKeyword,
213+
node.interfaceKeyword,
214+
node.finalKeyword,
215+
node.sealedKeyword,
216+
node.mixinKeyword,
217+
],
218+
node.classKeyword,
219+
node.name,
220+
node.typeParameters,
221+
node.extendsClause,
222+
node.withClause,
223+
node.implementsClause,
224+
node.nativeClause,
225+
node.leftBracket,
226+
node.members,
227+
node.rightBracket);
198228
}
199229

200230
@override
@@ -375,7 +405,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
375405

376406
@override
377407
void visitExtendsClause(ExtendsClause node) {
378-
throw UnimplementedError();
408+
assert(false, 'This node is handled by PieceFactory.createType().');
379409
}
380410

381411
@override
@@ -601,7 +631,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
601631

602632
@override
603633
void visitImplementsClause(ImplementsClause node) {
604-
throw UnimplementedError();
634+
assert(false, 'This node is handled by PieceFactory.createType().');
605635
}
606636

607637
@override
@@ -794,7 +824,10 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
794824

795825
@override
796826
void visitNativeClause(NativeClause node) {
797-
throw UnimplementedError();
827+
space();
828+
token(node.nativeKeyword);
829+
space();
830+
visit(node.name);
798831
}
799832

800833
@override
@@ -1268,7 +1301,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
12681301

12691302
@override
12701303
void visitWithClause(WithClause node) {
1271-
throw UnimplementedError();
1304+
assert(false, 'This node is handled by PieceFactory.createType().');
12721305
}
12731306

12741307
@override

lib/src/front_end/delimited_list_builder.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
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/front_end/comment_writer.dart';
76

87
import '../comment_type.dart';
98
import '../piece/list.dart';
109
import '../piece/piece.dart';
10+
import 'comment_writer.dart';
1111
import 'piece_factory.dart';
1212

1313
/// Incrementally builds a [ListPiece], handling commas, comments, and

lib/src/front_end/piece_factory.dart

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import '../piece/infix.dart';
1515
import '../piece/list.dart';
1616
import '../piece/piece.dart';
1717
import '../piece/postfix.dart';
18+
import '../piece/type.dart';
1819
import 'ast_node_visitor.dart';
1920
import 'comment_writer.dart';
2021
import 'delimited_list_builder.dart';
@@ -59,24 +60,44 @@ mixin PieceFactory implements CommentWriter {
5960
/// if (condition) {
6061
/// } else {}
6162
/// ```
62-
void createBlock(Block block, {bool forceSplit = false}) {
63-
token(block.leftBracket);
63+
void createBody(Token leftBracket, List<AstNode> contents, Token rightBracket,
64+
{bool forceSplit = false}) {
65+
token(leftBracket);
6466
var leftBracketPiece = pieces.split();
6567

6668
var sequence = SequenceBuilder(this);
67-
for (var node in block.statements) {
69+
for (var node in contents) {
6870
sequence.visit(node);
71+
72+
// If the node has a non-empty braced body, then require a blank line
73+
// between it and the next node.
74+
if (node.hasNonEmptyBody) sequence.addBlank();
6975
}
7076

7177
// Place any comments before the "}" inside the block.
72-
sequence.addCommentsBefore(block.rightBracket);
78+
sequence.addCommentsBefore(rightBracket);
7379

74-
token(block.rightBracket);
80+
token(rightBracket);
7581
var rightBracketPiece = pieces.take();
7682

7783
pieces.give(BlockPiece(
7884
leftBracketPiece, sequence.build(), rightBracketPiece,
79-
alwaysSplit: forceSplit || block.statements.isNotEmpty));
85+
alwaysSplit: forceSplit || contents.isNotEmpty));
86+
}
87+
88+
/// Creates a [BlockPiece] for a given [Block].
89+
///
90+
/// If [forceSplit] is `true`, then the block will split even if empty. This
91+
/// is used, for example, with empty blocks in `if` statements followed by
92+
/// `else` clauses:
93+
///
94+
/// ```
95+
/// if (condition) {
96+
/// } else {}
97+
/// ```
98+
void createBlock(Block block, {bool forceSplit = false}) {
99+
createBody(block.leftBracket, block.statements, block.rightBracket,
100+
forceSplit: forceSplit);
80101
}
81102

82103
/// Creates a piece for a `break` or `continue` statement.
@@ -387,6 +408,74 @@ mixin PieceFactory implements CommentWriter {
387408
pieces.give(builder.build());
388409
}
389410

411+
/// Creates a class, enum, extension, etc. declaration with a body containing
412+
/// members.
413+
void createType(
414+
NodeList<Annotation> metadata,
415+
List<Token?> modifiers,
416+
Token keyword,
417+
Token name,
418+
TypeParameterList? typeParameters,
419+
ExtendsClause? extendsClause,
420+
WithClause? withClause,
421+
ImplementsClause? implementsClause,
422+
NativeClause? nativeClause,
423+
Token leftBracket,
424+
List<AstNode> members,
425+
Token rightBracket) {
426+
if (metadata.isNotEmpty) throw UnimplementedError('Type metadata.');
427+
if (members.isNotEmpty) throw UnimplementedError('Type members.');
428+
429+
modifiers.forEach(modifier);
430+
token(keyword);
431+
space();
432+
token(name);
433+
visit(typeParameters);
434+
var header = pieces.split();
435+
436+
var clauses = <ClausePiece>[];
437+
438+
void typeClause(Token keyword, List<AstNode> types) {
439+
token(keyword);
440+
var keywordPiece = pieces.split();
441+
442+
var typePieces = <Piece>[];
443+
for (var type in types) {
444+
visit(type);
445+
commaAfter(type);
446+
typePieces.add(pieces.split());
447+
}
448+
449+
clauses.add(ClausePiece(keywordPiece, typePieces));
450+
}
451+
452+
if (extendsClause != null) {
453+
typeClause(extendsClause.extendsKeyword, [extendsClause.superclass]);
454+
}
455+
456+
if (withClause != null) {
457+
typeClause(withClause.withKeyword, withClause.mixinTypes);
458+
}
459+
460+
if (implementsClause != null) {
461+
typeClause(
462+
implementsClause.implementsKeyword, implementsClause.interfaces);
463+
}
464+
465+
ClausesPiece? clausesPiece;
466+
if (clauses.isNotEmpty) {
467+
clausesPiece =
468+
ClausesPiece(clauses, allowLeadingClause: extendsClause != null);
469+
}
470+
471+
visit(nativeClause);
472+
space();
473+
createBody(leftBracket, members, rightBracket);
474+
var body = pieces.take();
475+
476+
pieces.give(TypePiece(header, clausesPiece, body));
477+
}
478+
390479
/// Creates a [ListPiece] for a type argument or type parameter list.
391480
void createTypeList(
392481
Token leftBracket, Iterable<AstNode> elements, Token rightBracket) {

lib/src/piece/block.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class BlockPiece extends Piece {
2828
}
2929

3030
@override
31-
List<State> get additionalStates => const [State.split];
31+
List<State> get additionalStates => [if (contents.isNotEmpty) State.split];
3232

3333
@override
3434
void format(CodeWriter writer, State state) {

lib/src/piece/clause.dart

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,20 +70,49 @@ import 'piece.dart';
7070
/// This ensures that when any wrapping occurs, the keywords are always at the
7171
/// beginning of the line.
7272
class ClausesPiece extends Piece {
73+
/// State where we split between the clauses but not before the first one.
74+
static const State _betweenClauses = State(1);
75+
7376
final List<ClausePiece> _clauses;
7477

75-
ClausesPiece(this._clauses);
78+
/// If `true`, then we're allowed to split between the clauses without
79+
/// splitting before the first one too.
80+
///
81+
/// This is used for class declarations where the `extends` clauses is treated
82+
/// a little specially because it's a deeper coupling to the class and so we
83+
/// want it to stay on the top line even if the other clauses split, like:
84+
///
85+
/// ```
86+
/// class BaseClass extends Derived
87+
/// implements OtherThing {
88+
/// ...
89+
/// }
90+
/// ```
91+
final bool _allowLeadingClause;
92+
93+
ClausesPiece(this._clauses, {bool allowLeadingClause = false})
94+
: _allowLeadingClause = allowLeadingClause;
7695

7796
@override
78-
List<State> get additionalStates => const [State.split];
97+
List<State> get additionalStates =>
98+
[if (_allowLeadingClause) _betweenClauses, State.split];
7999

80100
@override
81101
void format(CodeWriter writer, State state) {
82-
// If any of the lists inside any of the clauses split, split at the
83-
// keywords too.
84-
writer.setAllowNewlines(state == State.split);
85102
for (var clause in _clauses) {
86-
writer.splitIf(state == State.split, indent: Indent.expression);
103+
if (_allowLeadingClause && clause == _clauses.first) {
104+
// Before the leading clause, only split when in the fully split state.
105+
// A split inside the first clause forces a split before the keyword.
106+
writer.splitIf(state == State.split, indent: Indent.expression);
107+
writer.setAllowNewlines(state == State.split);
108+
} else {
109+
// For the other clauses (or if there is no leading one), split in the
110+
// fully split state and any split inside and clause forces all of them
111+
// to split.
112+
writer.setAllowNewlines(state != State.unsplit);
113+
writer.splitIf(state != State.unsplit, indent: Indent.expression);
114+
}
115+
87116
writer.format(clause);
88117
}
89118
}
@@ -112,6 +141,9 @@ class ClausePiece extends Piece {
112141

113142
@override
114143
void format(CodeWriter writer, State state) {
144+
// If any of the parts inside the clause split, split the list.
145+
writer.setAllowNewlines(state != State.unsplit);
146+
115147
writer.format(_keyword);
116148
for (var part in _parts) {
117149
writer.splitIf(state == State.split, indent: Indent.expression);

lib/src/piece/type.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 'clause.dart';
6+
import 'piece.dart';
7+
8+
/// Piece for a type declaration with a body containing members.
9+
///
10+
/// Used for class, enum, and extension declarations.
11+
class TypePiece extends Piece {
12+
/// The leading keywords and modifiers, type name, and type parameters.
13+
final Piece _header;
14+
15+
/// The `extends`, `with`, and/or `implements` clauses, if there are any.
16+
final ClausesPiece? _clauses;
17+
18+
/// The `native` clause, if any, and the type body.
19+
final Piece _body;
20+
21+
TypePiece(this._header, this._clauses, this._body);
22+
23+
@override
24+
void format(CodeWriter writer, State state) {
25+
writer.format(_header);
26+
if (_clauses case var clauses?) writer.format(clauses);
27+
writer.space();
28+
writer.format(_body);
29+
}
30+
31+
@override
32+
void forEachChild(void Function(Piece piece) callback) {
33+
callback(_header);
34+
if (_clauses case var clauses?) callback(clauses);
35+
callback(_body);
36+
}
37+
38+
@override
39+
String toString() => 'Type';
40+
}

0 commit comments

Comments
 (0)