Skip to content

Commit bdc8a8c

Browse files
Correct indentation on parameters with both metadata and default values. (#1478)
Correct indentation on parameters with both metadata and default values. The AST node for a parameter with both metadata and a default value looks like: DefaultValueFormalParameter FormalParameter The inner node has the metadata and the outer one has the default value. Prior to this PR, that meant that we'd create an AssignPiece for the default value whose left-hand side piece was the function parameter and its metadata. That meant that when the metadata split, the outer AssignPiece would be forced to split. This slightly awkward change makes the piece structure reflect how the user (and, alas, not Analyzer) thinks of the syntax where the metadata is part of the entire parameter and the default value is inside the parameter. Fix #1461. Co-authored-by: Nate Bosch <[email protected]>
1 parent 1cc5a25 commit bdc8a8c

File tree

6 files changed

+146
-59
lines changed

6 files changed

+146
-59
lines changed

lib/src/front_end/ast_node_visitor.dart

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -465,12 +465,9 @@ class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {
465465

466466
@override
467467
void visitDefaultFormalParameter(DefaultFormalParameter node) {
468-
if (node.separator case var separator?) {
469-
writeAssignment(node.parameter, separator, node.defaultValue!,
470-
spaceBeforeOperator: separator.type == TokenType.EQ);
471-
} else {
472-
pieces.visit(node.parameter);
473-
}
468+
// Visit the inner parameter. It will then access its parent to extract the
469+
// default value.
470+
pieces.visit(node.parameter);
474471
}
475472

476473
@override

lib/src/front_end/piece_factory.dart

Lines changed: 102 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -527,8 +527,16 @@ mixin PieceFactory {
527527
/// If [fieldKeyword] and [period] are given, the former should be the `this`
528528
/// or `super` keyword for an initializing formal or super parameter.
529529
void writeFormalParameter(
530-
NormalFormalParameter node, TypeAnnotation? type, Token? name,
530+
FormalParameter node, TypeAnnotation? type, Token? name,
531531
{Token? mutableKeyword, Token? fieldKeyword, Token? period}) {
532+
// If the parameter has a default value, the parameter node will be wrapped
533+
// in a DefaultFormalParameter node containing the default.
534+
(Token separator, Expression value)? defaultValueRecord;
535+
if (node.parent
536+
case DefaultFormalParameter(:var separator?, :var defaultValue?)) {
537+
defaultValueRecord = (separator, defaultValue);
538+
}
539+
532540
writeParameter(
533541
metadata: node.metadata,
534542
modifiers: [
@@ -539,7 +547,8 @@ mixin PieceFactory {
539547
type,
540548
fieldKeyword: fieldKeyword,
541549
period: period,
542-
name);
550+
name,
551+
defaultValue: defaultValueRecord);
543552
}
544553

545554
/// Writes a function, method, getter, or setter declaration.
@@ -604,10 +613,38 @@ mixin PieceFactory {
604613
});
605614
}
606615

616+
/// If [parameter] has a [defaultValue] then writes a piece for the parameter
617+
/// followed by that default value.
618+
///
619+
/// Otherwise, just writes [parameter].
620+
void writeDefaultValue(
621+
Piece parameter, (Token separator, Expression value)? defaultValue) {
622+
if (defaultValue == null) {
623+
pieces.add(parameter);
624+
return;
625+
}
626+
627+
var (separator, value) = defaultValue;
628+
var operatorPiece = pieces.build(() {
629+
if (separator.type == TokenType.EQ) pieces.space();
630+
pieces.token(separator);
631+
if (separator.type != TokenType.EQ) pieces.space();
632+
});
633+
634+
var valuePiece = nodePiece(value, context: NodeContext.assignment);
635+
636+
pieces.add(AssignPiece(
637+
left: parameter,
638+
operatorPiece,
639+
valuePiece,
640+
canBlockSplitRight: value.canBlockSplit));
641+
}
642+
607643
/// Writes a function type or function-typed formal.
608644
///
609645
/// If creating a piece for a function-typed formal, then [parameter] is the
610-
/// formal parameter.
646+
/// formal parameter. If there is a default value, then [defaultValue] is
647+
/// the `=` or `:` separator followed by the constant expression.
611648
///
612649
/// If this is a function-typed initializing formal (`this.foo()`), then
613650
/// [fieldKeyword] is `this` and [period] is the `.`. Likewise, for a
@@ -621,44 +658,58 @@ mixin PieceFactory {
621658
{FormalParameter? parameter,
622659
Token? fieldKeyword,
623660
Token? period}) {
624-
Piece? returnTypePiece;
625-
if (parameter != null && returnType != null) {
626-
// Attach any parameter metadata and modifiers to the return type.
627-
returnTypePiece =
628-
pieces.build(metadata: parameter.metadata, inlineMetadata: true, () {
629-
pieces.modifier(parameter.requiredKeyword);
630-
pieces.modifier(parameter.covariantKeyword);
631-
pieces.visit(returnType);
632-
});
633-
} else if (returnType != null) {
634-
returnTypePiece = nodePiece(returnType);
661+
// If the type is a function-typed parameter with a default value, then
662+
// grab the default value from the parent node.
663+
(Token separator, Expression value)? defaultValueRecord;
664+
if (parameter?.parent
665+
case DefaultFormalParameter(:var separator?, :var defaultValue?)) {
666+
defaultValueRecord = (separator, defaultValue);
635667
}
636668

637-
// If there's no return type, attach the metadata to the signature.
638-
var signatureMetadata = const <Annotation>[];
639-
if (parameter != null && returnType == null) {
640-
signatureMetadata = parameter.metadata;
641-
}
669+
var metadata = parameter?.metadata ?? const <Annotation>[];
670+
pieces.withMetadata(metadata, inlineMetadata: true, () {
671+
Piece? returnTypePiece;
672+
if (returnType != null) {
673+
returnTypePiece = pieces.build(() {
674+
// Attach any parameter modifiers to the return type.
675+
if (parameter != null) {
676+
pieces.modifier(parameter.requiredKeyword);
677+
pieces.modifier(parameter.covariantKeyword);
678+
}
642679

643-
var signature =
644-
pieces.build(metadata: signatureMetadata, inlineMetadata: true, () {
645-
// If there's no return type, attach the parameter modifiers to the
646-
// signature.
647-
if (parameter != null && returnType == null) {
648-
pieces.modifier(parameter.requiredKeyword);
649-
pieces.modifier(parameter.covariantKeyword);
680+
pieces.visit(returnType);
681+
});
650682
}
651683

652-
pieces.token(fieldKeyword);
653-
pieces.token(period);
654-
pieces.token(functionKeywordOrName);
655-
pieces.visit(typeParameters);
656-
pieces.visit(parameters);
657-
pieces.token(question);
658-
});
684+
var signature = pieces.build(() {
685+
// If there's no return type, attach the parameter modifiers to the
686+
// signature.
687+
if (parameter != null && returnType == null) {
688+
pieces.modifier(parameter.requiredKeyword);
689+
pieces.modifier(parameter.covariantKeyword);
690+
}
659691

660-
pieces.add(FunctionPiece(returnTypePiece, signature,
661-
isReturnTypeFunctionType: returnType is GenericFunctionType));
692+
pieces.token(fieldKeyword);
693+
pieces.token(period);
694+
pieces.token(functionKeywordOrName);
695+
pieces.visit(typeParameters);
696+
pieces.visit(parameters);
697+
pieces.token(question);
698+
});
699+
700+
var function = FunctionPiece(returnTypePiece, signature,
701+
isReturnTypeFunctionType: returnType is GenericFunctionType);
702+
703+
// TODO(rnystrom): It would be good if the AssignPiece created for the
704+
// default value could treat the parameter list on the left-hand side as
705+
// block-splittable. But since it's a FunctionPiece and not directly a
706+
// ListPiece, AssignPiece doesn't support block-splitting it. If #1466 is
707+
// fixed, that may enable us to handle block-splitting here too. In
708+
// practice, it doesn't really matter since function-typed formals are
709+
// deprecated, default values on function-typed parameters are rare, and
710+
// when both occur, they rarely split.
711+
writeDefaultValue(function, defaultValueRecord);
712+
});
662713
}
663714

664715
/// Writes a piece for the header -- everything from the `if` keyword to the
@@ -1316,11 +1367,15 @@ mixin PieceFactory {
13161367
/// Writes a piece for a parameter-like constructor: Either a simple formal
13171368
/// parameter or a record type field, which is syntactically similar to a
13181369
/// parameter.
1370+
///
1371+
/// If the parameter has a default value, then [defaultValue] contains the
1372+
/// `:` or `=` separator and the constant value expression.
13191373
void writeParameter(TypeAnnotation? type, Token? name,
13201374
{List<Annotation> metadata = const [],
13211375
List<Token?> modifiers = const [],
13221376
Token? fieldKeyword,
1323-
Token? period}) {
1377+
Token? period,
1378+
(Token separator, Expression value)? defaultValue}) {
13241379
// Begin a piece to attach metadata to the parameter.
13251380
pieces.withMetadata(metadata, inlineMetadata: true, () {
13261381
Piece? typePiece;
@@ -1351,14 +1406,20 @@ mixin PieceFactory {
13511406
});
13521407
}
13531408

1409+
Piece parameterPiece;
13541410
if (typePiece != null && namePiece != null) {
13551411
// We have both a type and name, allow splitting between them.
1356-
pieces.add(VariablePiece(typePiece, [namePiece], hasType: true));
1357-
} else if (typePiece != null) {
1358-
pieces.add(typePiece);
1359-
} else if (namePiece != null) {
1360-
pieces.add(namePiece);
1412+
parameterPiece = VariablePiece(typePiece, [namePiece], hasType: true);
1413+
} else {
1414+
// Will have at least a type or name.
1415+
parameterPiece = typePiece ?? namePiece!;
13611416
}
1417+
1418+
// If there's a default value, include it. We do that inside here so that
1419+
// any metadata surrounds the entire assignment instead of being part of
1420+
// the assignment's left-hand side where a split in the metadata would
1421+
// force a split at the default value separator.
1422+
writeDefaultValue(parameterPiece, defaultValue);
13621423
});
13631424
}
13641425

test/tall/function/default_value.unit

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,30 @@ function([
4545
],
4646
]) {
4747
body;
48-
}
48+
}
49+
>>> Function typed parameter with default value.
50+
f([callback() = constantFunction]) {}
51+
<<<
52+
f([callback() = constantFunction]) {}
53+
>>> Split in function typed parameter parameter list with default value.
54+
f([callback(SomeLongType longParameter, AnotherType anotherParameter) = constantFunction]) {}
55+
<<<
56+
### TODO(rnystrom): The output isn't ideal here, but code like this is very
57+
### rare. See the comment in PieceFactory.writeFunctionType().
58+
f([
59+
callback(
60+
SomeLongType longParameter,
61+
AnotherType anotherParameter,
62+
) =
63+
constantFunction,
64+
]) {}
65+
>>> Split in function typed parameter default value.
66+
f([callback(SomeType parameter) = const CallableClass(someLongConstantArgument, anotherConstantArgument)]) {}
67+
<<<
68+
f([
69+
callback(SomeType parameter) =
70+
const CallableClass(
71+
someLongConstantArgument,
72+
anotherConstantArgument,
73+
),
74+
]) {}

test/tall/function/metadata.unit

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,12 @@ class Foo {
8787
@bar super.field, [
8888
@foo() @baz super.another,
8989
]);
90-
}
90+
}
91+
>>> Function typed parameter with default value.
92+
f([@metadata @another(argument, argument) callback() = constantFunction]) {}
93+
<<<
94+
f([
95+
@metadata
96+
@another(argument, argument)
97+
callback() = constantFunction,
98+
]) {}

test/tall/regression/0200/0247.unit

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212
@Option(help: 'The git Uri containing the jefe.yaml.', abbr: 'g')
1313
String gitUri,
1414
@Option(help: 'The directory to install into', abbr: 'd')
15-
String installDirectory:
16-
'.',
15+
String installDirectory: '.',
1716
@Flag(help: 'Skips the checkout of the develop branch', abbr: 's')
18-
bool skipCheckout:
19-
false,
17+
bool skipCheckout: false,
2018
}) async {}

test/tall/regression/0300/0387.unit

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,12 @@ greet(@Rest(valueHelp: 'who', help: 'Name(s) to greet.') List<String> who,
77
@Option(name: 'greeting', help: 'Alternate word to greet with e.g. "Hi".')
88
String salutation: 'Hello'}) {}
99
<<<
10-
### TODO(1461): The indentation is wrong here.
1110
greet(
1211
@Rest(valueHelp: 'who', help: 'Name(s) to greet.') List<String> who, {
1312
@Group.start(title: 'Output')
14-
@Option(help: 'How many !\'s to append.')
15-
int enthusiasm:
16-
0,
13+
@Option(help: 'How many !\'s to append.')
14+
int enthusiasm: 0,
1715
@Flag(abbr: 'l', help: 'Put names on separate lines.') bool lineMode: false,
1816
@Option(name: 'greeting', help: 'Alternate word to greet with e.g. "Hi".')
19-
String salutation:
20-
'Hello',
17+
String salutation: 'Hello',
2118
}) {}

0 commit comments

Comments
 (0)