Skip to content

Commit 34dd26f

Browse files
authored
Format constructor declarations. (#1338)
Format constructor declarations. Includes everything that can go in a constructor: - Factory and redirecting constructors. - Initializing formals ("this." parameters). - Super parameters. - Constructor initializers. - Assert initializers. Constructors are tricky because there are some constraints between how the parameter list and initializer list are allowed to split. Up until now, aside from setAllowNewlines(), there aren't really any ways that pieces interact. To support this, I added a pretty simple API where when a piece calls CodeWriter.format() on a child piece, it can pass it what state the piece is required to be in. If the solution doesn't have the child in that state, it invalidates the solution. It seems to work pretty well. There is probably a more optimal way to implement that in the solver so that we test these constraints even before formatting and discard the solution more eagerly. I left a TODO comment about that and we can look into it later when we're benchmarking.
1 parent 6249194 commit 34dd26f

15 files changed

+1175
-95
lines changed

lib/src/back_end/code_writer.dart

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,16 @@ class CodeWriter {
5353
/// The number of characters in the line currently being written.
5454
int _column = 0;
5555

56-
/// Whether this solution has encountered a newline where none is allowed.
56+
/// Whether this solution has encountered a conflict that makes it not a valid
57+
/// solution.
58+
///
59+
/// This can occur if:
5760
///
58-
/// If true, it means the solution is invalid.
59-
bool _containsInvalidNewline = false;
61+
/// - A mandatory newline occurs like from a line comment or statement where
62+
/// none is permitted.
63+
/// - An outer piece requires a child piece to have a certain state and it
64+
/// doesn't.
65+
bool _isInvalid = false;
6066

6167
/// The stack of state for each [Piece] being formatted.
6268
///
@@ -126,7 +132,7 @@ class CodeWriter {
126132

127133
return Solution(_pieceStates, _buffer.toString(), _selectionStart,
128134
_selectionEnd, _nextPieceToExpand,
129-
isValid: !_containsInvalidNewline, overflow: _overflow, cost: _cost);
135+
isValid: !_isInvalid, overflow: _overflow, cost: _cost);
130136
}
131137

132138
/// Notes that a newline has been written.
@@ -138,7 +144,7 @@ class CodeWriter {
138144
/// the raw text contains a newline, which can happen in multi-line block
139145
/// comments and multi-line string literals.
140146
void handleNewline() {
141-
if (!_options.allowNewlines) _containsInvalidNewline = true;
147+
if (!_options.allowNewlines) _isInvalid = true;
142148

143149
// Note that this piece contains a newline so that we can propagate that
144150
// up to containing pieces too.
@@ -228,19 +234,33 @@ class CodeWriter {
228234

229235
/// Format [piece] and insert the result into the code being written and
230236
/// returned by [finish()].
231-
void format(Piece piece) {
232-
// Don't bother recursing into the piece tree if we know the solution will
233-
// be discarded.
234-
if (_containsInvalidNewline) return;
235-
237+
///
238+
/// If [requireState] is given, then [piece] must be in that state or the
239+
/// solution is considered invalid. This is used to create constraints
240+
/// between pieces. For example, a constructor piece forces the initializer
241+
/// list child piece to split if the parameter list splits.
242+
void format(Piece piece, {State? requireState}) {
236243
_pieceOptions.add(_PieceOptions(_options.indent, _options.allowNewlines));
237244

238245
var isUnsolved = !_pieceStates.isBound(piece) && piece.states.length > 1;
239246
if (isUnsolved) _currentUnsolvedPieces.add(piece);
240247

241248
var state = _pieceStates.pieceState(piece);
242249

243-
_cost += state.cost;
250+
// If the parent piece constrains this child to a certain state, then
251+
// invalidate the solution if it doesn't match.
252+
if (requireState != null && state != requireState) {
253+
_isInvalid = true;
254+
// TODO(perf): The solver doesn't discard invalid states that are caused
255+
// by newlines because sometimes an invalid state like that can lead to
256+
// a valid state by selecting values for other pieces.
257+
//
258+
// But in this case, once a partial solution is invalidated by one piece's
259+
// state conflicting with another's, any solution derived from that is
260+
// also going to be invalid and we can prune that entire solution tree.
261+
}
262+
263+
_cost += piece.stateCost(state);
244264

245265
// TODO(perf): Memoize this. Might want to create a nested PieceWriter
246266
// instead of passing in `this` so we can better control what state needs
@@ -316,7 +336,7 @@ class CodeWriter {
316336
// expand it next.
317337
if (!_foundExpandLine &&
318338
_nextPieceToExpand != null &&
319-
(_column > _pageWidth || _containsInvalidNewline)) {
339+
(_column > _pageWidth || _isInvalid)) {
320340
// We found a problematic line, so remember it and the piece on it.
321341
_foundExpandLine = true;
322342
} else if (!_foundExpandLine) {

lib/src/front_end/ast_node_visitor.dart

Lines changed: 148 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import '../piece/adjacent.dart';
1313
import '../piece/assign.dart';
1414
import '../piece/block.dart';
1515
import '../piece/chain.dart';
16+
import '../piece/constructor.dart';
1617
import '../piece/for.dart';
1718
import '../piece/if.dart';
1819
import '../piece/infix.dart';
@@ -130,7 +131,17 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
130131

131132
@override
132133
Piece visitAssertInitializer(AssertInitializer node) {
133-
throw UnimplementedError();
134+
return buildPiece((b) {
135+
b.token(node.assertKeyword);
136+
b.add(createList(
137+
leftBracket: node.leftParenthesis,
138+
[
139+
node.condition,
140+
if (node.message case var message?) message,
141+
],
142+
rightBracket: node.rightParenthesis,
143+
));
144+
});
134145
}
135146

136147
@override
@@ -343,18 +354,85 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
343354

344355
@override
345356
Piece visitConstructorDeclaration(ConstructorDeclaration node) {
346-
throw UnimplementedError();
357+
var header = buildPiece((b) {
358+
b.modifier(node.externalKeyword);
359+
b.modifier(node.constKeyword);
360+
b.modifier(node.factoryKeyword);
361+
b.visit(node.returnType);
362+
b.token(node.period);
363+
b.token(node.name);
364+
});
365+
366+
var parameters = nodePiece(node.parameters);
367+
368+
Piece? redirect;
369+
Piece? initializerSeparator;
370+
Piece? initializers;
371+
if (node.redirectedConstructor case var constructor?) {
372+
redirect = AssignPiece(
373+
tokenPiece(node.separator!), nodePiece(constructor),
374+
isValueDelimited: false);
375+
} else if (node.initializers.isNotEmpty) {
376+
initializerSeparator = tokenPiece(node.separator!);
377+
initializers = createList(node.initializers,
378+
style: const ListStyle(commas: Commas.nonTrailing));
379+
}
380+
381+
var body = createFunctionBody(node.body);
382+
383+
return ConstructorPiece(header, parameters, body,
384+
canSplitParameters: node.parameters.parameters
385+
.canSplit(node.parameters.rightParenthesis),
386+
hasOptionalParameter: node.parameters.rightDelimiter != null,
387+
redirect: redirect,
388+
initializerSeparator: initializerSeparator,
389+
initializers: initializers);
347390
}
348391

349392
@override
350393
Piece visitConstructorFieldInitializer(ConstructorFieldInitializer node) {
351-
throw UnimplementedError();
394+
return buildPiece((b) {
395+
b.token(node.thisKeyword);
396+
b.token(node.period);
397+
b.add(createAssignment(node.fieldName, node.equals, node.expression));
398+
});
352399
}
353400

354401
@override
355402
Piece visitConstructorName(ConstructorName node) {
356-
throw UnsupportedError(
357-
'This node is handled by visitInstanceCreationExpression().');
403+
// If there is an import prefix and/or constructor name, then allow
404+
// splitting before the `.`. This doesn't look good, but is consistent with
405+
// constructor calls that don't have `new` or `const`. We allow splitting
406+
// in the latter because there is no way to distinguish syntactically
407+
// between a named constructor call and any other kind of method call or
408+
// property access.
409+
var operations = <Piece>[];
410+
411+
var builder = AdjacentBuilder(this);
412+
if (node.type.importPrefix case var importPrefix?) {
413+
builder.token(importPrefix.name);
414+
operations.add(builder.build());
415+
builder.token(importPrefix.period);
416+
}
417+
418+
// The name of the type being constructed.
419+
var type = node.type;
420+
builder.token(type.name2);
421+
builder.visit(type.typeArguments);
422+
builder.token(type.question);
423+
424+
// If this is a named constructor, the name.
425+
if (node.name != null) {
426+
operations.add(builder.build());
427+
builder.token(node.period);
428+
builder.visit(node.name);
429+
}
430+
431+
// If there was a prefix or constructor name, then make a splittable piece.
432+
// Otherwise, the current piece is a simple identifier for the name.
433+
operations.add(builder.build());
434+
if (operations.length == 1) return operations.first;
435+
return ChainPiece(operations);
358436
}
359437

360438
@override
@@ -550,7 +628,30 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
550628

551629
@override
552630
Piece visitFieldFormalParameter(FieldFormalParameter node) {
553-
throw UnimplementedError();
631+
if (node.parameters case var parameters?) {
632+
// A function-typed field formal like:
633+
//
634+
// ```
635+
// C(this.fn(parameter));
636+
// ```
637+
return createFunctionType(
638+
node.type,
639+
fieldKeyword: node.thisKeyword,
640+
period: node.period,
641+
node.name,
642+
node.typeParameters,
643+
parameters,
644+
node.question,
645+
parameter: node);
646+
} else {
647+
return createFormalParameter(
648+
node,
649+
mutableKeyword: node.keyword,
650+
fieldKeyword: node.thisKeyword,
651+
period: node.period,
652+
node.type,
653+
node.name);
654+
}
554655
}
555656

556657
@override
@@ -736,7 +837,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
736837
@override
737838
Piece visitFunctionDeclaration(FunctionDeclaration node) {
738839
return createFunction(
739-
externalKeyword: node.externalKeyword,
840+
modifiers: [node.externalKeyword],
740841
returnType: node.returnType,
741842
propertyKeyword: node.propertyKeyword,
742843
name: node.name,
@@ -974,11 +1075,9 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
9741075
@override
9751076
Piece visitMethodDeclaration(MethodDeclaration node) {
9761077
return createFunction(
977-
externalKeyword: node.externalKeyword,
978-
modifierKeyword: node.modifierKeyword,
1078+
modifiers: [node.externalKeyword, node.modifierKeyword],
9791079
returnType: node.returnType,
980-
operatorKeyword: node.operatorKeyword,
981-
propertyKeyword: node.propertyKeyword,
1080+
propertyKeyword: node.operatorKeyword ?? node.propertyKeyword,
9821081
name: node.name,
9831082
typeParameters: node.typeParameters,
9841083
parameters: node.parameters,
@@ -1166,7 +1265,12 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
11661265
@override
11671266
Piece visitRedirectingConstructorInvocation(
11681267
RedirectingConstructorInvocation node) {
1169-
throw UnimplementedError();
1268+
return buildPiece((b) {
1269+
b.token(node.thisKeyword);
1270+
b.token(node.period);
1271+
b.visit(node.constructorName);
1272+
b.visit(node.argumentList);
1273+
});
11701274
}
11711275

11721276
@override
@@ -1260,22 +1364,8 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
12601364

12611365
@override
12621366
Piece visitSimpleFormalParameter(SimpleFormalParameter node) {
1263-
var builder = AdjacentBuilder(this);
1264-
startFormalParameter(node, builder);
1265-
builder.modifier(node.keyword);
1266-
1267-
if ((node.type, node.name) case (var _?, var name?)) {
1268-
// Have both a type and name, so allow splitting after the type.
1269-
builder.visit(node.type);
1270-
var typePiece = builder.build();
1271-
var namePiece = tokenPiece(name);
1272-
return VariablePiece(typePiece, [namePiece], hasType: true);
1273-
} else {
1274-
// Don't have both a type and name, so just write whichever one we have.
1275-
builder.visit(node.type);
1276-
builder.token(node.name);
1277-
return builder.build();
1278-
}
1367+
return createFormalParameter(node, node.type, node.name,
1368+
mutableKeyword: node.keyword);
12791369
}
12801370

12811371
@override
@@ -1300,7 +1390,12 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
13001390

13011391
@override
13021392
Piece visitSuperConstructorInvocation(SuperConstructorInvocation node) {
1303-
throw UnimplementedError();
1393+
return buildPiece((b) {
1394+
b.token(node.superKeyword);
1395+
b.token(node.period);
1396+
b.visit(node.constructorName);
1397+
b.visit(node.argumentList);
1398+
});
13041399
}
13051400

13061401
@override
@@ -1310,7 +1405,30 @@ class AstNodeVisitor extends ThrowingAstVisitor<Piece> with PieceFactory {
13101405

13111406
@override
13121407
Piece visitSuperFormalParameter(SuperFormalParameter node) {
1313-
throw UnimplementedError();
1408+
if (node.parameters case var parameters?) {
1409+
// A function-typed super parameter like:
1410+
//
1411+
// ```
1412+
// C(super.fn(parameter));
1413+
// ```
1414+
return createFunctionType(
1415+
node.type,
1416+
fieldKeyword: node.superKeyword,
1417+
period: node.period,
1418+
node.name,
1419+
node.typeParameters,
1420+
parameters,
1421+
node.question,
1422+
parameter: node);
1423+
} else {
1424+
return createFormalParameter(
1425+
node,
1426+
mutableKeyword: node.keyword,
1427+
fieldKeyword: node.superKeyword,
1428+
period: node.period,
1429+
node.type,
1430+
node.name);
1431+
}
13141432
}
13151433

13161434
@override

lib/src/front_end/delimited_list_builder.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ class DelimitedListBuilder {
5555
var blockElement = -1;
5656
if (_style.allowBlockElement) blockElement = _findBlockElement();
5757

58-
return ListPiece(_leftBracket, _elements, _blanksAfter, _rightBracket,
59-
_style, blockElement,
60-
mustSplit: _mustSplit);
58+
var piece = ListPiece(_leftBracket, _elements, _blanksAfter, _rightBracket,
59+
_style, blockElement);
60+
if (_mustSplit) piece.pin(State.split);
61+
return piece;
6162
}
6263

6364
/// Adds the opening [bracket] to the built list.

0 commit comments

Comments
 (0)