diff --git a/lib/src/front_end/ast_node_visitor.dart b/lib/src/front_end/ast_node_visitor.dart index a1fc65a5..4aed128d 100644 --- a/lib/src/front_end/ast_node_visitor.dart +++ b/lib/src/front_end/ast_node_visitor.dart @@ -1511,19 +1511,59 @@ class AstNodeVisitor extends ThrowingAstVisitor with PieceFactory { @override Piece visitRecordTypeAnnotation(RecordTypeAnnotation node) { - throw UnimplementedError(); + var namedFields = node.namedFields; + var positionalFields = node.positionalFields; + + // Single positional record types always have a trailing comma. + var listStyle = positionalFields.length == 1 && namedFields == null + ? const ListStyle(commas: Commas.alwaysTrailing) + : const ListStyle(commas: Commas.trailing); + var builder = DelimitedListBuilder(this, listStyle); + + // If all parameters are optional, put the `{` right after `(`. + if (positionalFields.isEmpty && namedFields != null) { + builder.leftBracket( + node.leftParenthesis, + delimiter: namedFields.leftBracket, + ); + } else { + builder.leftBracket(node.leftParenthesis); + } + + for (var positionalField in positionalFields) { + builder.visit(positionalField); + } + + Token? rightDelimiter; + if (namedFields != null) { + // If we have both positional fields and named fields, then we need to add + // the left bracket delimiter before the first named field. + if (positionalFields.isNotEmpty) { + builder.leftDelimiter(namedFields.leftBracket); + } + for (var namedField in namedFields.fields) { + builder.visit(namedField); + } + rightDelimiter = namedFields.rightBracket; + } + + builder.rightBracket(node.rightParenthesis, delimiter: rightDelimiter); + return buildPiece((b) { + b.add(builder.build()); + b.token(node.question); + }); } @override Piece visitRecordTypeAnnotationNamedField( RecordTypeAnnotationNamedField node) { - throw UnimplementedError(); + return createRecordTypeField(node); } @override Piece visitRecordTypeAnnotationPositionalField( RecordTypeAnnotationPositionalField node) { - throw UnimplementedError(); + return createRecordTypeField(node); } @override diff --git a/lib/src/front_end/piece_factory.dart b/lib/src/front_end/piece_factory.dart index 687f7844..048f9860 100644 --- a/lib/src/front_end/piece_factory.dart +++ b/lib/src/front_end/piece_factory.dart @@ -225,24 +225,14 @@ mixin PieceFactory { var builder = AdjacentBuilder(this); startFormalParameter(node, builder); builder.modifier(mutableKeyword); - builder.visit(type); - - Piece? typePiece; - if (type != null && name != null) { - typePiece = builder.build(); - } - - builder.token(fieldKeyword); - builder.token(period); - builder.token(name); - - // If we have both a type and name, allow splitting between them. - if (typePiece != null) { - var namePiece = builder.build(); - return VariablePiece(typePiece, [namePiece], hasType: true); - } - - return builder.build(); + return finishTypeAndName( + type, + name, + builder, + mutableKeyword: mutableKeyword, + fieldKeyword: fieldKeyword, + period: period, + ); } /// Creates a function, method, getter, or setter declaration. @@ -542,6 +532,17 @@ mixin PieceFactory { return builder.build(); } + /// Creates an [AdjacentPiece] for a given record type field. + Piece createRecordTypeField(RecordTypeAnnotationField node) { + // TODO(tall): Format metadata. + if (node.metadata.isNotEmpty) throw UnimplementedError(); + return finishTypeAndName( + node.type, + node.name, + AdjacentBuilder(this), + ); + } + /// Creates a class, enum, extension, mixin, or mixin application class /// declaration. /// @@ -653,6 +654,34 @@ mixin PieceFactory { style: const ListStyle(commas: Commas.nonTrailing, splitCost: 3)); } + /// Creates a [VariablePiece] that allows splitting between a type and a name, + /// if they both exist. + /// + /// Otherwise, finishes building the existing [AdjacentPiece] with the + /// [builder]. + Piece finishTypeAndName( + TypeAnnotation? type, Token? name, AdjacentBuilder builder, + {Token? mutableKeyword, Token? fieldKeyword, Token? period}) { + builder.visit(type); + + Piece? typePiece; + if (type != null && name != null) { + typePiece = builder.build(); + } + + builder.token(fieldKeyword); + builder.token(period); + builder.token(name); + + // If we have both a type and name, allow splitting between them. + if (typePiece != null) { + var namePiece = builder.build(); + return VariablePiece(typePiece, [namePiece], hasType: true); + } + + return builder.build(); + } + /// Writes the parts of a formal parameter shared by all formal parameter /// types: metadata, `covariant`, etc. void startFormalParameter( diff --git a/test/type/function.stmt b/test/type/function.stmt index dd0064b1..75ad4c6d 100644 --- a/test/type/function.stmt +++ b/test/type/function.stmt @@ -218,6 +218,56 @@ longMethod({ required int reallyLongParameterNameWow, }) {} +>>> Record type with multiple fields in parameter has no trailing comma. +function((TypeName, TypeName) parameter) { ; } +<<< +function( + (TypeName, TypeName) parameter, +) { + ; +} +>>> Split single long positional record type field. +function((VeryLongTypeName___________________,) param) {;} +<<< +function( + ( + VeryLongTypeName___________________, + ) param, +) { + ; +} +>>> Split inside parameter list with record type. +function((TypeName, TypeName, TypeName, TypeName, TypeName) record) {;} +<<< +function( + ( + TypeName, + TypeName, + TypeName, + TypeName, + TypeName, + ) record, +) { + ; +} +>>> Single positional has a trailing comma inside parameter list with record type. +function((TypeName,) record) {;} +<<< +function((TypeName,) record) { + ; +} +>>> Named parameter has no trailing comma inside parameter list with record type. +function(({TypeName param,}) record) {;} +<<< +function(({TypeName param}) record) { + ; +} +>>> Multiple positional fields have no trailing comma in parameter list with record type. +function((TypeName,TypeName,) record,) {;} +<<< +function((TypeName, TypeName) record) { + ; +} >>> Unsplit generic method instantiation. void main() => id < int > ; <<< diff --git a/test/type/record.stmt b/test/type/record.stmt new file mode 100644 index 00000000..efdbf0be --- /dev/null +++ b/test/type/record.stmt @@ -0,0 +1,145 @@ +40 columns | +>>> Empty record. +( ) x; +<<< +() x; +>>> Empty nullable record type. +( ) ? x; +<<< +()? x; +>>> Nullable record type. +( int , bool ) ? x; +<<< +(int, bool)? x; +>>> Single positional field. +( int , ) x; +<<< +(int,) x; +>>> Single named field. +( { int n } ) x; +<<< +({int n}) x; +>>> Named positional fields. +( int value , String label) x; +<<< +(int value, String label) x; +>>> Unnamed positional fields. +( int , String ) x; +<<< +(int, String) x; +>>> Named fields. +( { int value , String label } ) x; +<<< +({int value, String label}) x; +>>> Split between the type and the name. +( VeryVeryLongType_____ veryLongName___________________ , ) x; +<<< +( + VeryVeryLongType_____ + veryLongName___________________, +) x; +>>> Split named positional fields. +( int longValue , String veryVeryLongLabel , ) x; +<<< +( + int longValue, + String veryVeryLongLabel, +) x; +>>> Unsplit unnamed positional fields have no trailing comma. +( int , String , ) x; +<<< +(int, String) x; +>>> Split only named fields. +( { int longValue , String veryLongLabel , } ) x; +<<< +({ + int longValue, + String veryLongLabel, +}) x; +>>> Empty record types don't split. +someLongFunctionName__________________(() x) {} +<<< +someLongFunctionName__________________( + () x, +) {} +>>> Unsplit short single positional field. +(TypeName, +) x; +<<< +(TypeName,) x; +>>> Unsplit single positional field. +(VeryLongTypeName________________,) x; +<<< +(VeryLongTypeName________________,) x; +>>> Split positional types. +(TypeName,TypeName,TypeName,TypeName) x; +<<< +( + TypeName, + TypeName, + TypeName, + TypeName, +) x; +>>> Split named types. +({TypeName a,TypeName b,TypeName c,TypeName d}) x; +<<< +({ + TypeName a, + TypeName b, + TypeName c, + TypeName d, +}) x; +>>> Split named if positional splits. +(TypeName,TypeName,TypeName,TypeName,{TypeName a,TypeName b}) x; +<<< +( + TypeName, + TypeName, + TypeName, + TypeName, { + TypeName a, + TypeName b, +}) x; +>>> Split positional if named splits. +(TypeName,TypeName,{TypeName a,TypeName b,TypeName c,TypeName d}) x; +<<< +( + TypeName, + TypeName, { + TypeName a, + TypeName b, + TypeName c, + TypeName d, +}) x; +>>> Single named field has no trailing comma. +({int n,}) x; +<<< +({int n}) x; +>>> Multiple positional fields have no trailing comma. +(int m, int n,) x; +<<< +(int m, int n) x; +>>> Split outer record if inner record splits. +((TypeName,TypeName,TypeName,TypeName),TypeName) x; +<<< +( + ( + TypeName, + TypeName, + TypeName, + TypeName, + ), + TypeName, +) x; +>>> Split outer type argument list if inner record splits. +Map map; +<<< +Map< + String, + ( + TypeName, + TypeName, + TypeName, + TypeName, + ) +> map; diff --git a/test/type/record_comment.stmt b/test/type/record_comment.stmt new file mode 100644 index 00000000..4bb27584 --- /dev/null +++ b/test/type/record_comment.stmt @@ -0,0 +1,65 @@ +40 columns | +>>> Comment between the type and name of a field. +(int // comment +value, String label) x; +<<< +( + int // comment + value, + String label, +) x; +>>> Comment after field and comma. +(int value, // comment +String label) x; +<<< +( + int value, // comment + String label, +) x; +>>> Comment before field and comma. +(int value // comment +,String label) x; +<<< +( + int value, // comment + String label, +) x; +>>> Comment between positional and named delimiter. +(int value, // comment +{String label}) +x; +<<< +( + int value, { // comment + String label, +}) x; +>>> Comment after named left delimiter. +(int value, { // comment +String label}) +x; +<<< +( + int value, { // comment + String label, +}) x; +>>> Comment after named right delimiter. +(int value, {String label} // comment +) +x; +<<< +( + int value, { + String label, // comment +}) x; +>>> Comment between record type and nullable question mark. +(int value , ) // comment +? x; +<<< +(int value,) // comment +? x; +>>> Comment after record type. +(int value, String label) // comment +x; +<<< +(int value, String label) // comment +x;