Skip to content

Commit ee3dd8a

Browse files
authored
Format function types. (#1288)
* Format function types. This involves formatting parameter lists which is a big piece of work. They work similar to argument lists and collection literals... except for the special syntax around `[ ... ]` and `{ ... }` for optional parameters. Getting that working right involved adding some more functionality to DelimitedListBuilder and ListPiece. Getting comments working with that syntax was particularly difficult. I ended up deciding that the formatter can move comments before or after the optional parameter delimiters if it needs to. Doing so produces better looking output, as in: ``` // Before: f(// weird comment location [param] // another ); // After: f([ // weird comment location param, // another ]); ``` It also, I think is cleaner to implement than trying to laboriously keep the comment where the user authored it. In the process of reworking how comments are handled in lists, I also tweaked how inline block comments are handled before commas. Previously, they would get moved after the comma, like: ``` // Before: [element /* comment */, another]; // After: [element, /* comment */ another]; ``` Now, they stay where they were, which is I think what users want. A common use case for block comments in argument/collection lists is describing the meaning of a preceding null argument and this keeps that. Line after an element but before the comma continue to be moved after the comma: ``` // Before: f(param // comment ,); // After: f( param, // comment ); ``` Also: - Added tests for the loosened behavior of equalIgnoringWhitespace(). - Added a test for a corner case of variable splitting that I ran into with parameters, which use the same VariablePiece. * Add TODO comment to investigate something better than equalIgnoringWhitespace(). * Apply review feedback.
1 parent a528dc5 commit ee3dd8a

15 files changed

+1054
-93
lines changed

lib/src/back_end/code_writer.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ class CodeWriter {
8181
/// comments and multi-line string literals.
8282
void handleNewline() {
8383
if (!_options.allowNewlines) _containsInvalidNewline = true;
84+
85+
// Note that this piece contains a newline so that we can propagate that
86+
// up to containing pieces too.
87+
_options.hasNewline = true;
8488
}
8589

8690
/// Appends [text] to the output.
@@ -158,7 +162,7 @@ class CodeWriter {
158162
}
159163

160164
/// Sets whether newlines are allowed to occur from this point on for the
161-
/// current piece or any of its children.
165+
/// current piece.
162166
void setAllowNewlines(bool allowed) {
163167
_options.allowNewlines = allowed;
164168
}
@@ -186,7 +190,12 @@ class CodeWriter {
186190
// to be used as the key in the memoization table.
187191
piece.format(this, state);
188192

189-
_pieceOptions.removeLast();
193+
var childOptions = _pieceOptions.removeLast();
194+
195+
// If the child [piece] contains a newline then this one transitively does.
196+
// TODO(tall): At some point, we may want to provide an API so that pieces
197+
// can block this from propagating outward.
198+
if (childOptions.hasNewline) handleNewline();
190199
}
191200

192201
/// Format [piece] if not null.
@@ -221,5 +230,8 @@ class _PieceOptions {
221230
/// considered invalid and gets discarded.
222231
bool allowNewlines;
223232

233+
/// Whether any newlines have occurred in this piece or any of its children.
234+
bool hasNewline = false;
235+
224236
_PieceOptions(this.indent, this.nesting, this.allowNewlines);
225237
}

lib/src/front_end/ast_node_visitor.dart

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,11 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
267267

268268
@override
269269
void visitDefaultFormalParameter(DefaultFormalParameter node) {
270-
throw UnimplementedError();
270+
visit(node.parameter);
271+
272+
// TODO(tall): Implement default values when function declarations are
273+
// implemented.
274+
if (node.separator != null) throw UnimplementedError();
271275
}
272276

273277
@override
@@ -357,7 +361,29 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
357361

358362
@override
359363
void visitFormalParameterList(FormalParameterList node) {
360-
throw UnimplementedError();
364+
// Find the first non-mandatory parameter (if there are any).
365+
var firstOptional =
366+
node.parameters.indexWhere((p) => p is DefaultFormalParameter);
367+
368+
// If all parameters are optional, put the `[` or `{` right after `(`.
369+
var builder = DelimitedListBuilder(this);
370+
if (node.parameters.isNotEmpty && firstOptional == 0) {
371+
builder.leftBracket(node.leftParenthesis, delimiter: node.leftDelimiter);
372+
} else {
373+
builder.leftBracket(node.leftParenthesis);
374+
}
375+
376+
for (var i = 0; i < node.parameters.length; i++) {
377+
// If this is the first optional parameter, put the delimiter before it.
378+
if (firstOptional > 0 && i == firstOptional) {
379+
builder.leftDelimiter(node.leftDelimiter!);
380+
}
381+
382+
builder.add(node.parameters[i]);
383+
}
384+
385+
builder.rightBracket(node.rightParenthesis, delimiter: node.rightDelimiter);
386+
writer.push(builder.build());
361387
}
362388

363389
@override
@@ -432,12 +458,15 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
432458

433459
@override
434460
void visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) {
435-
throw UnimplementedError();
461+
startFormalParameter(node);
462+
createFunctionType(node.returnType, node.name, node.typeParameters,
463+
node.parameters, node.question);
436464
}
437465

438466
@override
439467
void visitGenericFunctionType(GenericFunctionType node) {
440-
throw UnimplementedError();
468+
createFunctionType(node.returnType, node.functionKeyword,
469+
node.typeParameters, node.parameters, node.question);
441470
}
442471

443472
@override
@@ -586,15 +615,8 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
586615
visit(node.methodName);
587616
visit(node.typeArguments);
588617

589-
var builder = DelimitedListBuilder(this);
590-
builder.leftBracket(node.argumentList.leftParenthesis);
591-
592-
for (var argument in node.argumentList.arguments) {
593-
builder.add(argument);
594-
}
595-
596-
builder.rightBracket(node.argumentList.rightParenthesis);
597-
writer.push(builder.build());
618+
createDelimited(node.argumentList.leftParenthesis,
619+
node.argumentList.arguments, node.argumentList.rightParenthesis);
598620
}
599621

600622
@override
@@ -814,7 +836,30 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
814836

815837
@override
816838
void visitSimpleFormalParameter(SimpleFormalParameter node) {
817-
throw UnimplementedError();
839+
startFormalParameter(node);
840+
841+
if (node.keyword != null) throw UnimplementedError();
842+
843+
// TODO(tall): When function declarations are implemented, test that the
844+
// formatter won't split after `var` or `final` in a parameter (with a
845+
// type or not).
846+
847+
if ((node.type, node.name) case (var type?, var name?)) {
848+
// Have both a type and name, so allow splitting between them.
849+
visit(type);
850+
var typePiece = writer.pop();
851+
writer.split();
852+
853+
token(name);
854+
var namePiece = writer.pop();
855+
writer.split();
856+
857+
writer.push(VariablePiece(typePiece, [namePiece], hasType: true));
858+
} else {
859+
// Only one of name or type so just write whichever there is.
860+
visit(node.type);
861+
token(node.name);
862+
}
818863
}
819864

820865
@override
@@ -901,25 +946,22 @@ class AstNodeVisitor extends ThrowingAstVisitor<void>
901946

902947
@override
903948
void visitTypeArgumentList(TypeArgumentList node) {
904-
var builder = DelimitedListBuilder(this);
905-
builder.leftBracket(node.leftBracket);
906-
907-
for (var arguments in node.arguments) {
908-
builder.add(arguments);
909-
}
910-
911-
builder.rightBracket(node.rightBracket);
912-
writer.push(builder.build());
949+
createDelimited(node.leftBracket, node.arguments, node.rightBracket);
913950
}
914951

915952
@override
916953
void visitTypeParameter(TypeParameter node) {
917-
throw UnimplementedError();
954+
token(node.name);
955+
if (node.bound case var bound?) {
956+
writer.space();
957+
modifier(node.extendsKeyword);
958+
visit(bound);
959+
}
918960
}
919961

920962
@override
921963
void visitTypeParameterList(TypeParameterList node) {
922-
throw UnimplementedError();
964+
createDelimited(node.leftBracket, node.typeParameters, node.rightBracket);
923965
}
924966

925967
@override

lib/src/front_end/comment_writer.dart

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ mixin CommentWriter {
8585
writer.writeComment(comment);
8686
}
8787

88-
if (comment.type == CommentType.line) writer.writeNewline();
88+
if (comment.type == CommentType.line || comment.type == CommentType.doc) {
89+
writer.writeNewline();
90+
}
8991
}
9092

9193
if (comments.isNotEmpty && _needsSpaceAfterComment(token.lexeme)) {
@@ -130,6 +132,10 @@ mixin CommentWriter {
130132
} else if (comment.type == TokenType.SINGLE_LINE_COMMENT) {
131133
type = CommentType.line;
132134
} else if (commentLine == previousLine || commentLine == tokenLine) {
135+
// TODO(tall): I'm not sure if it makes sense to distinguish block
136+
// comments with newlines around them from other block comments in the
137+
// new Piece representation. Consider merging CommentType.inlineBlock
138+
// and CommentType.block into a single type.
133139
type = CommentType.inlineBlock;
134140
} else {
135141
type = CommentType.block;
@@ -191,8 +197,10 @@ class SourceComment {
191197
SourceComment(this.text, this.type, {required this.flushLeft});
192198

193199
/// Whether this comment contains a mandatory newline, either because it's a
194-
/// line comment or a multi-line block comment.
195-
bool get containsNewline => type == CommentType.line || text.contains('\n');
200+
/// comment that should be on its own line or a lexeme with a newline inside
201+
/// it (i.e. multi-line block comment, multi-line string).
202+
bool get containsNewline =>
203+
type != CommentType.inlineBlock || text.contains('\n');
196204

197205
@override
198206
String toString() =>
@@ -261,9 +269,10 @@ class CommentSequence extends ListBase<SourceComment> {
261269
if (linesBefore(commentIndex) != 0) return false;
262270

263271
// Doc comments and non-inline `/* ... */` comments are always pushed to
264-
// the next line.
272+
// the next line. Only inline block comments and line comments are allowed
273+
// to hang at the end of a line.
265274
var type = _comments[commentIndex].type;
266-
return type != CommentType.doc && type != CommentType.block;
275+
return type == CommentType.inlineBlock || type == CommentType.line;
267276
}
268277

269278
/// Whether the comment at [commentIndex] should be attached to the following

0 commit comments

Comments
 (0)