Skip to content

Commit c179702

Browse files
authored
Format top-level and local variable declarations. (#1280)
Format top-level and local variable declarations. This required tweaking the way partial solutions are ordered so that the solver doesn't avoid overflowing solutions that will ultimately lead to non-overflowing lower cost ones.
1 parent 35a26b2 commit c179702

File tree

11 files changed

+400
-47
lines changed

11 files changed

+400
-47
lines changed

lib/src/back_end/solution.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,12 @@ class Score implements Comparable<Score> {
152152
if (!isValid) return 1;
153153
if (!other.isValid) return -1;
154154

155-
// Overflow is always penalized more than line splitting cost.
156-
if (overflow != other.overflow) return overflow.compareTo(other.overflow);
155+
// Even though overflow is "worse" than cost, we order in terms of cost
156+
// because a solution with overflow may lead to a low-cost solution without
157+
// overflow, so we want to explore in cost order.
158+
if (cost != other.cost) return cost.compareTo(other.cost);
157159

158-
return cost.compareTo(other.cost);
160+
return overflow.compareTo(other.overflow);
159161
}
160162

161163
@override

lib/src/front_end/ast_node_visitor.dart

Lines changed: 67 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import 'package:analyzer/dart/ast/visitor.dart';
66
import 'package:analyzer/source/line_info.dart';
77

88
import '../dart_formatter.dart';
9+
import '../piece/piece.dart';
10+
import '../piece/variable.dart';
911
import '../source_code.dart';
1012
import 'comment_writer.dart';
1113
import 'delimited_list_builder.dart';
@@ -34,15 +36,49 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
3436
AstNodeVisitor(DartFormatter formatter, this.lineInfo, SourceCode source)
3537
: writer = PieceWriter(formatter, source);
3638

37-
/// Runs the visitor on [node], formatting its contents.
39+
/// Visits [node] and returns the formatted result.
3840
///
3941
/// Returns a [SourceCode] containing the resulting formatted source and
4042
/// updated selection, if any.
4143
///
4244
/// This is the only method that should be called externally. Everything else
4345
/// is effectively private.
4446
SourceCode run(AstNode node) {
45-
visit(node);
47+
// Always treat the code being formatted as contained in a sequence, even
48+
// if we aren't formatting an entire compilation unit. That way, comments
49+
// before and after the node are handled properly.
50+
var sequence = SequenceBuilder(this);
51+
52+
if (node is CompilationUnit) {
53+
if (node.scriptTag case var scriptTag?) {
54+
sequence.add(scriptTag);
55+
sequence.addBlank();
56+
}
57+
58+
// Put a blank line between the library tag and the other directives.
59+
Iterable<Directive> directives = node.directives;
60+
if (directives.isNotEmpty && directives.first is LibraryDirective) {
61+
sequence.add(directives.first);
62+
sequence.addBlank();
63+
directives = directives.skip(1);
64+
}
65+
66+
for (var directive in directives) {
67+
sequence.add(directive);
68+
}
69+
70+
for (var declaration in node.declarations) {
71+
sequence.add(declaration);
72+
}
73+
} else {
74+
// Just formatting a single statement.
75+
sequence.add(node);
76+
}
77+
78+
// Write any comments at the end of the code.
79+
sequence.addCommentsBefore(node.endToken.next!);
80+
81+
writer.push(sequence.build());
4682

4783
// Finish writing and return the complete result.
4884
return writer.finish();
@@ -167,32 +203,8 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
167203

168204
@override
169205
void visitCompilationUnit(CompilationUnit node) {
170-
var sequence = SequenceBuilder(this);
171-
172-
if (node.scriptTag case var scriptTag?) {
173-
sequence.add(scriptTag);
174-
sequence.addBlank();
175-
}
176-
177-
// Put a blank line between the library tag and the other directives.
178-
Iterable<Directive> directives = node.directives;
179-
if (directives.isNotEmpty && directives.first is LibraryDirective) {
180-
sequence.add(directives.first);
181-
sequence.addBlank();
182-
directives = directives.skip(1);
183-
}
184-
185-
for (var directive in directives) {
186-
sequence.add(directive);
187-
}
188-
189-
// TODO(tall): Handle top level declarations.
190-
if (node.declarations.isNotEmpty) throw UnimplementedError();
191-
192-
// Write any comments at the end of the file.
193-
sequence.addCommentsBefore(node.endToken.next!);
194-
195-
writer.push(sequence.build());
206+
throw UnsupportedError(
207+
'CompilationUnit should be handled directly by format().');
196208
}
197209

198210
@override
@@ -854,7 +866,8 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
854866

855867
@override
856868
void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
857-
throw UnimplementedError();
869+
visit(node.variables);
870+
token(node.semicolon);
858871
}
859872

860873
@override
@@ -879,17 +892,39 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
879892

880893
@override
881894
void visitVariableDeclaration(VariableDeclaration node) {
882-
throw UnimplementedError();
895+
token(node.name);
896+
finishAssignment(node.equals, node.initializer);
883897
}
884898

885899
@override
886900
void visitVariableDeclarationList(VariableDeclarationList node) {
887-
throw UnimplementedError();
901+
// TODO(tall): Format metadata.
902+
if (node.metadata.isNotEmpty) throw UnimplementedError();
903+
904+
modifier(node.lateKeyword);
905+
modifier(node.keyword);
906+
907+
// TODO(tall): Test how splits inside the type annotation (like in a type
908+
// argument list or a function type's parameter list) affect the indentation
909+
// and splitting of the surrounding variable declaration.
910+
visit(node.type);
911+
var header = writer.pop();
912+
913+
var variables = <Piece>[];
914+
for (var variable in node.variables) {
915+
writer.split();
916+
visit(variable);
917+
commaAfter(variable);
918+
variables.add(writer.pop());
919+
}
920+
921+
writer.push(VariablePiece(header, variables, hasType: node.type != null));
888922
}
889923

890924
@override
891925
void visitVariableDeclarationStatement(VariableDeclarationStatement node) {
892-
throw UnimplementedError();
926+
visit(node.variables);
927+
token(node.semicolon);
893928
}
894929

895930
@override

lib/src/front_end/piece_factory.dart

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,6 @@ typedef BinaryOperation = (AstNode left, Token operator, AstNode right);
4242
mixin PieceFactory implements CommentWriter {
4343
void visit(AstNode? node, {void Function()? before, void Function()? after});
4444

45-
/// Creates metadata annotations for a directive.
46-
///
47-
/// Always forces the annotations to be on a previous line.
48-
void createDirectiveMetadata(Directive directive) {
49-
// TODO(tall): Implement. See SourceVisitor._visitDirectiveMetadata().
50-
if (directive.metadata.isNotEmpty) throw UnimplementedError();
51-
}
52-
5345
/// Creates a [BlockPiece] for a given bracket-delimited block or declaration
5446
/// body.
5547
void createBlock(Token leftBracket, List<AstNode> nodes, Token rightBracket) {
@@ -81,6 +73,14 @@ mixin PieceFactory implements CommentWriter {
8173
alwaysSplit: nodes.isNotEmpty));
8274
}
8375

76+
/// Creates metadata annotations for a directive.
77+
///
78+
/// Always forces the annotations to be on a previous line.
79+
void createDirectiveMetadata(Directive directive) {
80+
// TODO(tall): Implement. See SourceVisitor._visitDirectiveMetadata().
81+
if (directive.metadata.isNotEmpty) throw UnimplementedError();
82+
}
83+
8484
/// Creates a dotted or qualified identifier.
8585
void createDotted(NodeList<SimpleIdentifier> components) {
8686
for (var component in components) {
@@ -229,6 +229,36 @@ mixin PieceFactory implements CommentWriter {
229229
writer.push(InfixPiece(operands));
230230
}
231231

232+
/// Creates a [Piece] for some code followed by an `=` and an expression in
233+
/// any place where an `=` appears:
234+
///
235+
/// * Assignment
236+
/// * Variable declaration
237+
/// * Constructor initializer
238+
///
239+
/// This method assumes the code to the left of the `=` has already been
240+
/// visited.
241+
///
242+
/// Does nothing if [equalsOperator] is `null`.
243+
void finishAssignment(Token? equalsOperator, Expression? rightHandSide) {
244+
if (equalsOperator == null) return;
245+
246+
writer.space();
247+
token(equalsOperator);
248+
var equals = writer.pop();
249+
writer.split();
250+
251+
visit(rightHandSide);
252+
253+
var initializer = writer.pop();
254+
writer.push(InfixPiece([equals, initializer]));
255+
}
256+
257+
/// Writes an optional modifier that precedes other code.
258+
void modifier(Token? keyword) {
259+
token(keyword, after: writer.space);
260+
}
261+
232262
/// Emit [token], along with any comments and formatted whitespace that comes
233263
/// before it.
234264
///

lib/src/front_end/sequence_builder.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,17 @@ class SequenceBuilder {
3636
/// Visits [node] and adds the resulting [Piece] to this sequence, handling
3737
/// any comments or blank lines that appear before it.
3838
void add(AstNode node) {
39-
addCommentsBefore(node.beginToken);
39+
var token = switch (node) {
40+
// If [node] is an [AnnotatedNode], then [beginToken] includes the
41+
// leading doc comment, which we want to handle separately. So, in that
42+
// case, explicitly skip past the doc comment to the subsequent metadata
43+
// (if there is any), or the beginning of the code.
44+
AnnotatedNode(metadata: [var annotation, ...]) => annotation.beginToken,
45+
AnnotatedNode() => node.firstTokenAfterCommentAndMetadata,
46+
_ => node.beginToken
47+
};
48+
49+
addCommentsBefore(token);
4050
_visitor.visit(node);
4151
_contents.add(_visitor.writer.pop());
4252
_visitor.writer.split();

lib/src/piece/postfix.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ class PostfixPiece extends Piece {
3030

3131
@override
3232
void format(CodeWriter writer, State state) {
33+
// If any of the operands split, then force the postfix sequence to split
34+
// too.
35+
// TODO(tall): This will need to be revisited when we use PostfixPiece for
36+
// actual postfix operators where this isn't always desired.
37+
if (state == State.initial) writer.setAllowNewlines(false);
38+
3339
for (var piece in pieces) {
3440
writer.splitIf(state == State.split, indent: Indent.expression);
3541
writer.format(piece);

lib/src/piece/variable.dart

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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 variable declaration.
9+
///
10+
/// Used for local variable declaration statements, top-level variable
11+
/// declarations and field declarations.
12+
///
13+
/// Typed and untyped variables have slightly different splitting logic.
14+
/// Untyped variables never split after the keyword but do indent subsequent
15+
/// variables:
16+
///
17+
/// ```
18+
/// var longVariableName = initializer,
19+
/// anotherVariable = anotherInitializer;
20+
/// ```
21+
///
22+
/// Typed variables can split that way too:
23+
///
24+
/// ```
25+
/// String longVariableName = initializer,
26+
/// anotherVariable = anotherInitializer;
27+
/// ```
28+
///
29+
/// But they can also split after the type annotation. When that happens, the
30+
/// variables aren't indented:
31+
///
32+
/// ```
33+
/// VeryLongTypeName
34+
/// longVariableName = initializer,
35+
/// anotherVariable = anotherInitializer;
36+
/// ```
37+
class VariablePiece extends Piece {
38+
/// Split between each variable in a multiple variable declaration.
39+
static const State _betweenVariables = State(1);
40+
41+
/// Split after the type annotation and between each variable.
42+
static const State _afterType = State(2);
43+
44+
/// The leading keywords (`var`, `final`, `late`) and optional type
45+
/// annotation.
46+
final Piece _header;
47+
48+
/// Each individual variable being declared.
49+
final List<Piece> _variables;
50+
51+
/// Whether the variable declaration has a type annotation.
52+
final bool _hasType;
53+
54+
/// Creates a [VariablePiece].
55+
///
56+
/// The [hasType] parameter should be `true` if the variable declaration has
57+
/// a type annotation.
58+
VariablePiece(this._header, this._variables, {required bool hasType})
59+
: _hasType = hasType;
60+
61+
@override
62+
List<State> get states => [
63+
if (_variables.length > 1) _betweenVariables,
64+
if (_hasType) _afterType,
65+
];
66+
67+
@override
68+
void format(CodeWriter writer, State state) {
69+
writer.format(_header);
70+
71+
// If we split at the variables (but not the type), then indent the
72+
// variables and their initializers.
73+
if (state == _betweenVariables) writer.setIndent(Indent.expression);
74+
75+
// Force variables to split if an initializer does.
76+
if (_variables.length > 1 && state == State.initial) {
77+
writer.setAllowNewlines(false);
78+
}
79+
80+
// Split after the type annotation.
81+
writer.splitIf(state == _afterType);
82+
83+
for (var i = 0; i < _variables.length; i++) {
84+
// Split between variables.
85+
if (i > 0) writer.splitIf(state != State.initial);
86+
87+
writer.format(_variables[i]);
88+
}
89+
}
90+
91+
@override
92+
void forEachChild(void Function(Piece piece) callback) {
93+
callback(_header);
94+
_variables.forEach(callback);
95+
}
96+
97+
@override
98+
String toString() => 'Var';
99+
}

lib/src/testing/test_file.dart

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@ class TestFile {
4848
factory TestFile._load(File file, String relativePath) {
4949
var lines = file.readAsLinesSync();
5050

51-
// Ignore comment lines.
52-
lines.removeWhere((line) => line.startsWith('###'));
53-
5451
// The first line may have a "|" to indicate the page width.
5552
var i = 0;
5653
int? pageWidth;

0 commit comments

Comments
 (0)