Primary constructors (#1807)

* Begin supporting primary constructors.

Since primary constructors adds a lot of new syntax and fairly complex
formatting for all of the things that can now go in a class header, I'm
breaking it into several commits.

This first commit only adds support for `;` as a class or extension type
body. That adds yet another wrinkle to writeType(), which was getting
very cumbersome, so I went ahead and split it out into a builder class
that makes it easier to mix and match the various things that can go in
the header and the different kinds of bodies.

* Format constructors defined using `new` or `factory`.

Since those are already allowed as modifiers, there isn't much to do
here except to add tests and remove the extra space if there is nothing
between the keyword and the parameter list.

* Format primary constructor parameter lists.

This is the big change. Now that we have both parameter lists and other
clauses in the class header, deciding how to split and indent them gets
a lot more complex.

This introduces a new piece to manage the constraints between the
parameter list and clauses. I think it produces pretty nice output and
generally avoids leaving the clauses hanging when the parameter list
splits.

This change also unifies how extension type representation clauses are
formatted with how primary constructors are formatted. Prior to this,
it had its own bespoke code path. That code path didn't follow the
exact same formatting rules as parameter lists and could look different
in weird cases. It also didn't have the nice rules this PR adds for
primary constructors.

In practice, I think it's rare for an extension type representation to
split and, when it does, I think this PR produces uniformly better
formatting. Because of that, I didn't language version that minor style
change. Preserving the old formatting for older language versions would
be pretty annoying because it would have to have a separate code path
for the extension extension type.

* Format declaring parameters.

Didn't require much implementation work because the language already
supported `final` and `var` on parameters (with different meaning), so
the formatter support was there.

* Set 3.13 as the anticipated release version for primary constructors.

(We can change it again if it ends up not being 3.13, but it definitely
won't be 3.11 or 3.12.)

* Tweak blank lines around top-level declarations.

* Don't force blank lines around declarations whose body is `;`. This
  allows:

  ```dart
  sealed class Expr;
  class IntExpr(final int value) extends Expr;
  class PlusExpr(final Expr left, final Expr right) extends Expr;
  ```

* Do force blank lines around mixin and extension types whose body is
  not `;`. The formatter should have always worked this way, but I
  overlooked mixins and extension types. Classes, enums, extensions all
  already are formatted this way.

* Test splitting around "const" or primary constructor name.

* Format primary constructor initializer bodies.

* Test that named and optional parameters work in a primary constructor.

* Add tests for preserving trailing commas in primary constructors.

* Apply review feedback.

* Fix duplicated 3.1.6 entry.

---------

Co-authored-by: Kallen Tu <kallentu@google.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 125cc0a..a3422e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,61 @@
+## 3.1.8-wip
+
+### Style changes
+
+* Format extension type representation clauses the same way primary constructor
+  formal parameter lists are formatted. This rarely makes a difference but
+  produces better formatting when the representation type is long and there are
+  other clauses on the extension type, as in:
+
+  ```dart
+  // Before:
+  extension type JSExportedDartFunction._(
+    JSExportedDartFunctionRepType _jsExportedDartFunction
+  )
+      implements JSFunction {}
+
+  // After:
+  extension type JSExportedDartFunction._(
+    JSExportedDartFunctionRepType _jsExportedDartFunction
+  ) implements JSFunction {}
+  ```
+
+  This change is *not* language versioned. (The old style is always worse, and
+  continuing to support it would add complexity to the formatter.)
+
+* Force blank lines around a mixin or extension type declaration if it doesn't
+  have a `;` body:
+
+  ```dart
+  // Before:
+  int above;
+  extension type Inches(int x) {}
+  mixin M {}
+  int below;
+
+  // After:
+  int above;
+
+  extension type Inches(int x) {}
+
+  mixin M {}
+
+  int below;
+  ```
+
+  The formatter already forces blank lines around class, enum, and extension
+  declarations. Mixins and extension types were overlooked. This makes them
+  consistent. This style change is language versioned and only affects
+  libraries at 3.13 or higher.
+
+  Note that the formatter allows classes and extension types whose body is `;`
+  to not have a blank line above or below them.
+
+### Internal changes
+
+* Support upcoming Dart language version 3.13.
+* Support formatting primary constructors.
+
 ## 3.1.7
 
 * Require `analyzer: '>=10.0.0 <12.0.0'`.
diff --git a/lib/src/dart_formatter.dart b/lib/src/dart_formatter.dart
index 20387c2..8064d88 100644
--- a/lib/src/dart_formatter.dart
+++ b/lib/src/dart_formatter.dart
@@ -35,7 +35,7 @@
 final class DartFormatter {
   /// The latest Dart language version that can be parsed and formatted by this
   /// version of the formatter.
-  static final latestLanguageVersion = Version(3, 12, 0);
+  static final latestLanguageVersion = Version(3, 13, 0);
 
   /// The latest Dart language version that will be formatted using the older
   /// "short" style.
diff --git a/lib/src/front_end/ast_node_visitor.dart b/lib/src/front_end/ast_node_visitor.dart
index d2178a8..e7582fc 100644
--- a/lib/src/front_end/ast_node_visitor.dart
+++ b/lib/src/front_end/ast_node_visitor.dart
@@ -18,7 +18,6 @@
 import '../piece/leading_comment.dart';
 import '../piece/list.dart';
 import '../piece/piece.dart';
-import '../piece/type.dart';
 import '../piece/type_parameter_bound.dart';
 import '../piece/variable.dart';
 import '../profile.dart';
@@ -30,6 +29,7 @@
 import 'piece_factory.dart';
 import 'piece_writer.dart';
 import 'sequence_builder.dart';
+import 'type_builder.dart';
 
 /// Visits every token of the AST and produces a tree of [Piece]s that
 /// corresponds to it and contains every token and comment in the original
@@ -108,18 +108,27 @@
       sequence.addBlank();
 
       for (var declaration in node.declarations) {
-        var hasBody =
-            declaration is ClassDeclaration ||
-            declaration is EnumDeclaration ||
-            declaration is ExtensionDeclaration;
+        // "Type" declarations that have braced bodies are surrounded by blank
+        // lines.
+        var addBlankLines = switch (declaration) {
+          ClassDeclaration(body: BlockClassBody()) => true,
+          EnumDeclaration() => true,
+          ExtensionDeclaration() => true,
+          ExtensionTypeDeclaration(body: BlockClassBody())
+              when style.blankLineAroundMixinAndExtensionTypes =>
+            true,
+          MixinDeclaration() when style.blankLineAroundMixinAndExtensionTypes =>
+            true,
+          _ => false,
+        };
 
         // Add a blank line before types with bodies.
-        if (hasBody) sequence.addBlank();
+        if (addBlankLines) sequence.addBlank();
 
         sequence.visit(declaration);
 
         // Add a blank line after type or function declarations with bodies.
-        if (hasBody || declaration.hasNonEmptyBody) sequence.addBlank();
+        if (addBlankLines || declaration.hasNonEmptyBody) sequence.addBlank();
       }
     } else {
       // Just formatting a single statement.
@@ -282,7 +291,8 @@
 
   @override
   void visitClassDeclaration(ClassDeclaration node) {
-    writeType(
+    var builder = TypeBuilder(
+      this,
       node.metadata,
       [
         node.abstractKeyword,
@@ -293,25 +303,19 @@
         node.mixinKeyword,
         node.classKeyword,
       ],
-      name: node.namePart.typeName,
-      typeParameters: node.namePart.typeParameters,
+      namePart: node.namePart,
       extendsClause: node.extendsClause,
       withClause: node.withClause,
       implementsClause: node.implementsClause,
       nativeClause: node.nativeClause,
-      body: () {
-        // TODO(scheglov): support for EmptyBody
-        var body = node.body as BlockClassBody;
-        return pieces.build(() {
-          writeBody(body.leftBracket, body.members, body.rightBracket);
-        });
-      },
     );
+    builder.buildClassBody(node.body);
   }
 
   @override
   void visitClassTypeAlias(ClassTypeAlias node) {
-    writeType(
+    var builder = TypeBuilder(
+      this,
       node.metadata,
       [
         node.abstractKeyword,
@@ -324,12 +328,13 @@
       ],
       name: node.name,
       typeParameters: node.typeParameters,
-      equals: node.equals,
-      superclass: node.superclass,
       withClause: node.withClause,
       implementsClause: node.implementsClause,
-      bodyType: TypeBodyType.semicolon,
-      body: () => tokenPiece(node.semicolon),
+    );
+    builder.buildMixinApplicationClass(
+      node.equals,
+      node.superclass,
+      node.semicolon,
     );
   }
 
@@ -457,9 +462,19 @@
       var header = pieces.build(() {
         pieces.modifier(node.externalKeyword);
         pieces.modifier(node.constKeyword);
-        pieces.modifier(node.newKeyword);
-        pieces.modifier(node.factoryKeyword);
-        pieces.visit(node.typeName);
+
+        // If there is no type name, then this is an unnamed constructor using
+        // `new` or `factory` to declare the constructor. In that case, don't
+        // put a space after the keyword.
+        if (node.typeName != null || node.name != null) {
+          pieces.modifier(node.newKeyword);
+          pieces.modifier(node.factoryKeyword);
+          pieces.visit(node.typeName);
+        } else {
+          pieces.token(node.newKeyword);
+          pieces.token(node.factoryKeyword);
+        }
+
         pieces.token(node.period);
         pieces.token(node.name);
       });
@@ -634,85 +649,15 @@
 
   @override
   void visitEnumDeclaration(EnumDeclaration node) {
-    writeType(
+    var builder = TypeBuilder(
+      this,
       node.metadata,
       [node.enumKeyword],
-      name: node.namePart.typeName,
-      typeParameters: node.namePart.typeParameters,
+      namePart: node.namePart,
       withClause: node.withClause,
       implementsClause: node.implementsClause,
-      bodyType: node.body.members.isEmpty
-          ? TypeBodyType.list
-          : TypeBodyType.block,
-      body: () {
-        if (node.body.members.isEmpty) {
-          // If there are no members, format the constants like a list. This
-          // keeps the enum declaration on one line if it fits.
-          var builder = DelimitedListBuilder(
-            this,
-            const ListStyle(spaceWhenUnsplit: true),
-          );
-
-          builder.leftBracket(node.body.leftBracket);
-          node.body.constants.forEach(builder.visit);
-          builder.rightBracket(
-            semicolon: node.body.semicolon,
-            node.body.rightBracket,
-          );
-          return builder.build(
-            forceSplit: style.preserveTrailingCommaBefore(
-              node.body.semicolon ?? node.body.rightBracket,
-            ),
-          );
-        } else {
-          // If there are members, format it like a block where each constant
-          // and member is on its own line.
-          var builder = SequenceBuilder(this);
-          builder.leftBracket(node.body.leftBracket);
-
-          // In 3.10 and later, if the source has a trailing comma before the
-          // `;`, it is preserved and the `;` is put on its own line. If there
-          // is no trailing comma in the source, the `;` stays on the same line
-          // as the last constant. Prior to 3.10, the behavior is the same as
-          // when preserved trailing commas is off: the last constant's comma
-          // is removed and the `;` is placed there instead.
-          var preserveTrailingComma =
-              style.preserveTrailingCommaAfterEnumValues &&
-              node.body.semicolon!.hasCommaBefore;
-          for (var constant in node.body.constants) {
-            var isLast = constant == node.body.constants.last;
-            builder.addCommentsBefore(constant.firstNonCommentToken);
-            builder.add(
-              createEnumConstant(
-                constant,
-                commaAfter: !isLast || preserveTrailingComma,
-                semicolon: isLast ? node.body.semicolon : null,
-              ),
-            );
-          }
-
-          // If we are preserving the trailing comma, then put the `;` on its
-          // own line after the last constant.
-          if (preserveTrailingComma) {
-            builder.add(tokenPiece(node.body.semicolon!));
-          }
-
-          // Insert a blank line between the constants and members.
-          builder.addBlank();
-
-          for (var node in node.body.members) {
-            builder.visit(node);
-
-            // If the node has a non-empty braced body, then require a blank
-            // line between it and the next node.
-            if (node.hasNonEmptyBody) builder.addBlank();
-          }
-
-          builder.rightBracket(node.body.rightBracket);
-          return builder.build();
-        }
-      },
     );
+    builder.buildEnum(node);
   }
 
   @override
@@ -787,44 +732,31 @@
 
   @override
   void visitExtensionDeclaration(ExtensionDeclaration node) {
-    (Token, TypeAnnotation)? onType;
-    if (node.onClause case var onClause?) {
-      onType = (onClause.onKeyword, onClause.extendedType);
-    }
-
-    writeType(
+    var builder = TypeBuilder(
+      this,
       node.metadata,
       [node.extensionKeyword],
       name: node.name,
       typeParameters: node.typeParameters,
-      onType: onType,
-      body: () {
-        return pieces.build(() {
-          writeBody(
-            node.body.leftBracket,
-            node.body.members,
-            node.body.rightBracket,
-          );
-        });
-      },
+      extensionOnClause: node.onClause,
+    );
+    builder.buildBlockBody(
+      node.body.leftBracket,
+      node.body.members,
+      node.body.rightBracket,
     );
   }
 
   @override
   void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) {
-    writeType(
+    var builder = TypeBuilder(
+      this,
       node.metadata,
       [node.extensionKeyword, node.typeKeyword],
-      primaryConstructor: node.primaryConstructor,
+      namePart: node.primaryConstructor,
       implementsClause: node.implementsClause,
-      body: () {
-        return pieces.build(() {
-          // TODO(scheglov): support for EmptyBody
-          var body = node.body as BlockClassBody;
-          writeBody(body.leftBracket, body.members, body.rightBracket);
-        });
-      },
     );
+    builder.buildClassBody(node.body);
   }
 
   @override
@@ -883,7 +815,12 @@
     }
 
     // If all parameters are optional, put the `[` or `{` right after `(`.
-    var builder = DelimitedListBuilder(this);
+    var listStyle = const ListStyle();
+    if (node.parent?.parent is ExtensionTypeDeclaration) {
+      listStyle = const ListStyle(commas: Commas.nonTrailing);
+    }
+
+    var builder = DelimitedListBuilder(this, listStyle);
 
     builder.addLeftBracket(
       pieces.build(() {
@@ -909,7 +846,7 @@
         forceSplit: style.preserveTrailingCommaBefore(
           node.rightDelimiter ?? node.rightParenthesis,
         ),
-        blockShaped: false,
+        blockShaped: node.parent is PrimaryConstructorDeclaration,
       ),
     );
   }
@@ -1539,22 +1476,19 @@
 
   @override
   void visitMixinDeclaration(MixinDeclaration node) {
-    writeType(
+    var builder = TypeBuilder(
+      this,
       node.metadata,
       [node.baseKeyword, node.mixinKeyword],
       name: node.name,
       typeParameters: node.typeParameters,
-      onClause: node.onClause,
+      mixinOnClause: node.onClause,
       implementsClause: node.implementsClause,
-      body: () {
-        return pieces.build(() {
-          writeBody(
-            node.body.leftBracket,
-            node.body.members,
-            node.body.rightBracket,
-          );
-        });
-      },
+    );
+    builder.buildBlockBody(
+      node.body.leftBracket,
+      node.body.members,
+      node.body.rightBracket,
     );
   }
 
@@ -1763,51 +1697,28 @@
   @override
   void visitPrimaryConstructorBody(PrimaryConstructorBody node) {
     pieces.withMetadata(node.metadata, () {
-      pieces.token(node.thisKeyword);
+      var header = pieces.build(() {
+        pieces.token(node.thisKeyword);
+      });
 
+      Piece? colon;
+      Piece? initializers;
       if (node.initializers.isNotEmpty) {
-        pieces.space();
-        pieces.token(node.colon);
-        pieces.space();
-        pieces.add(createCommaSeparated(node.initializers));
+        colon = tokenPiece(node.colon!);
+        initializers = createCommaSeparated(node.initializers);
       }
 
-      pieces.visit(node.body);
-    });
-  }
+      var body = nodePiece(node.body);
 
-  @override
-  void visitPrimaryConstructorDeclaration(PrimaryConstructorDeclaration node) {
-    pieces.modifier(node.constKeyword);
-    pieces.token(node.typeName);
-    pieces.visit(node.typeParameters);
-    pieces.visit(node.constructorName);
-
-    if (node.parent is ExtensionTypeDeclaration) {
-      var formalParameters = node.formalParameters;
-      var builder = DelimitedListBuilder(
-        this,
-        const ListStyle(commas: Commas.nonTrailing),
+      pieces.add(
+        ConstructorPiece.thisBlock(
+          header,
+          body,
+          initializerSeparator: colon,
+          initializers: initializers,
+        ),
       );
-      builder.leftBracket(formalParameters.leftParenthesis);
-      for (var formalParameter in formalParameters.parameters) {
-        // TODO(scheglov): support for optional formal parameters
-        formalParameter as SimpleFormalParameter;
-        builder.add(
-          pieces.build(() {
-            writeParameter(
-              metadata: formalParameter.metadata,
-              formalParameter.type,
-              formalParameter.name,
-            );
-          }),
-        );
-      }
-      builder.rightBracket(formalParameters.rightParenthesis);
-      pieces.add(builder.build());
-    } else {
-      pieces.visit(node.formalParameters);
-    }
+    });
   }
 
   @override
diff --git a/lib/src/front_end/formatting_style.dart b/lib/src/front_end/formatting_style.dart
index c796522..e733961 100644
--- a/lib/src/front_end/formatting_style.dart
+++ b/lib/src/front_end/formatting_style.dart
@@ -52,6 +52,14 @@
       _formatter.trailingCommas == TrailingCommas.preserve &&
       _languageVersion >= Version(3, 10, 0);
 
+  /// Whether mixin declarations and extension types with brace bodies should
+  /// always get a blank line above and below them.
+  ///
+  /// They always should have, but they were overlooked. We already do this for
+  /// classes, enums, and extensions.
+  bool get blankLineAroundMixinAndExtensionTypes =>
+      _languageVersion >= Version(3, 13, 0);
+
   /// Whether there is a trailing comma at the end of the list delimited by
   /// [rightBracket] which should be preserved by this style.
   bool preserveTrailingCommaBefore(Token rightBracket) =>
diff --git a/lib/src/front_end/piece_factory.dart b/lib/src/front_end/piece_factory.dart
index 8644cf0..61a306e 100644
--- a/lib/src/front_end/piece_factory.dart
+++ b/lib/src/front_end/piece_factory.dart
@@ -19,7 +19,6 @@
 import '../piece/piece.dart';
 import '../piece/prefix.dart';
 import '../piece/sequence.dart';
-import '../piece/type.dart';
 import '../piece/variable.dart';
 import 'chain_builder.dart';
 import 'comment_writer.dart';
@@ -840,12 +839,19 @@
   }) {
     var metadata = parameter?.metadata ?? const <Annotation>[];
     pieces.withMetadata(metadata, inlineMetadata: true, () {
+      var modifiers = [
+        parameter?.requiredKeyword,
+        parameter?.covariantKeyword,
+        if (parameter case FunctionTypedFormalParameter(:var keyword)) keyword,
+      ];
+
       void write() {
         // If there's no return type, attach the parameter modifiers to the
         // signature.
-        if (parameter != null && returnType == null) {
-          pieces.modifier(parameter.requiredKeyword);
-          pieces.modifier(parameter.covariantKeyword);
+        if (returnType == null) {
+          for (var modifier in modifiers) {
+            pieces.modifier(modifier);
+          }
         }
 
         pieces.token(fieldKeyword);
@@ -856,10 +862,6 @@
         pieces.token(question);
       }
 
-      var returnTypeModifiers = parameter != null
-          ? [parameter.requiredKeyword, parameter.covariantKeyword]
-          : const <Token?>[];
-
       // If the type is a function-typed parameter with a default value, then
       // grab the default value from the parent node and attach it to the
       // function.
@@ -868,12 +870,12 @@
         :var defaultValue?,
       )) {
         var function = pieces.build(() {
-          writeFunctionAndReturnType(returnTypeModifiers, returnType, write);
+          writeFunctionAndReturnType(modifiers, returnType, write);
         });
 
         writeDefaultValue(function, (separator, defaultValue));
       } else {
-        writeFunctionAndReturnType(returnTypeModifiers, returnType, write);
+        writeFunctionAndReturnType(modifiers, returnType, write);
       }
     });
   }
@@ -1423,119 +1425,6 @@
     );
   }
 
-  /// Writes a class, enum, extension, extension type, mixin, or mixin
-  /// application class declaration.
-  ///
-  /// The [keywords] list is the ordered list of modifiers and keywords at the
-  /// beginning of the declaration.
-  ///
-  /// For all but a mixin application class, [body] should a record containing
-  /// the bracket delimiters and the list of member declarations for the type's
-  /// body. For mixin application classes, [body] is `null` and instead
-  /// [equals], [superclass], and [semicolon] are provided.
-  ///
-  /// If the type is an extension, then [onType] is a record containing the
-  /// `on` keyword and the on type.
-  ///
-  /// If the type has a primary constructor, e.g. an extension type, then
-  /// [primaryConstructor] is not `null`.
-  void writeType(
-    NodeList<Annotation> metadata,
-    List<Token?> keywords, {
-    Token? name,
-    TypeParameterList? typeParameters,
-    Token? equals,
-    NamedType? superclass,
-    PrimaryConstructorDeclaration? primaryConstructor,
-    ExtendsClause? extendsClause,
-    MixinOnClause? onClause,
-    WithClause? withClause,
-    ImplementsClause? implementsClause,
-    NativeClause? nativeClause,
-    (Token, TypeAnnotation)? onType,
-    TypeBodyType bodyType = TypeBodyType.block,
-    required Piece Function() body,
-  }) {
-    // Begin a piece to attach the metadata to the type.
-    pieces.withMetadata(metadata, () {
-      var header = pieces.build(() {
-        var space = false;
-        for (var keyword in keywords) {
-          if (space) pieces.space();
-          pieces.token(keyword);
-          if (keyword != null) space = true;
-        }
-
-        pieces.token(name, spaceBefore: true);
-
-        if (typeParameters != null) {
-          pieces.visit(typeParameters);
-        }
-
-        // Mixin application classes have ` = Superclass` after the declaration
-        // name.
-        if (equals != null) {
-          pieces.space();
-          pieces.token(equals);
-          pieces.space();
-          pieces.visit(superclass!);
-        }
-
-        if (primaryConstructor != null) {
-          pieces.visit(primaryConstructor, spaceBefore: true);
-        }
-      });
-
-      var clauses = <Piece>[];
-
-      void typeClause(Token keyword, List<AstNode> types) {
-        clauses.add(
-          InfixPiece([
-            tokenPiece(keyword),
-            for (var type in types) nodePiece(type, commaAfter: true),
-          ], is3Dot7: style.is3Dot7),
-        );
-      }
-
-      if (extendsClause != null) {
-        typeClause(extendsClause.extendsKeyword, [extendsClause.superclass]);
-      }
-
-      if (onClause != null) {
-        typeClause(onClause.onKeyword, onClause.superclassConstraints);
-      }
-
-      if (withClause != null) {
-        typeClause(withClause.withKeyword, withClause.mixinTypes);
-      }
-
-      if (implementsClause != null) {
-        typeClause(
-          implementsClause.implementsKeyword,
-          implementsClause.interfaces,
-        );
-      }
-
-      if (onType case (var onKeyword, var onType)?) {
-        typeClause(onKeyword, [onType]);
-      }
-
-      if (nativeClause != null) {
-        typeClause(nativeClause.nativeKeyword, [?nativeClause.name]);
-      }
-
-      if (clauses.isNotEmpty) {
-        header = ClausePiece(
-          header,
-          clauses,
-          allowLeadingClause: extendsClause != null || onClause != null,
-        );
-      }
-
-      pieces.add(TypePiece(header, body(), bodyType: bodyType));
-    });
-  }
-
   /// Writes a [ListPiece] for a type argument or type parameter list.
   void writeTypeList(
     Token leftBracket,
diff --git a/lib/src/front_end/piece_writer.dart b/lib/src/front_end/piece_writer.dart
index b863311..4159f77 100644
--- a/lib/src/front_end/piece_writer.dart
+++ b/lib/src/front_end/piece_writer.dart
@@ -167,7 +167,7 @@
     _currentCode = null;
   }
 
-  /// Creates a returns a new piece.
+  /// Creates and returns a new piece.
   ///
   /// Invokes [buildCallback]. All tokens and AST nodes written during that
   /// callback are collected into the returned piece.
diff --git a/lib/src/front_end/type_builder.dart b/lib/src/front_end/type_builder.dart
new file mode 100644
index 0000000..d016c92
--- /dev/null
+++ b/lib/src/front_end/type_builder.dart
@@ -0,0 +1,295 @@
+// Copyright (c) 2026, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/token.dart';
+
+import '../ast_extensions.dart';
+import '../piece/clause.dart';
+import '../piece/infix.dart';
+import '../piece/list.dart';
+import '../piece/piece.dart';
+import '../piece/type.dart';
+import 'delimited_list_builder.dart';
+import 'piece_factory.dart';
+import 'sequence_builder.dart';
+
+/// Builds pieces for a class, enum, extension, extension type, mixin, or mixin
+/// application class declaration.
+final class TypeBuilder {
+  final PieceFactory _visitor;
+  final NodeList<Annotation> _metadata;
+  final List<Token?> _keywords;
+  final Token? _name;
+  final TypeParameterList? _typeParameters;
+  final ClassNamePart? _namePart;
+
+  final List<_Clause> _clauses;
+
+  /// Whether the first clause can be on the same line as the header even if
+  /// the other clauses split.
+  final bool _allowLeadingClause;
+
+  TypeBuilder(
+    this._visitor,
+    this._metadata,
+    this._keywords, {
+    Token? name,
+    TypeParameterList? typeParameters,
+    ClassNamePart? namePart,
+    ExtendsClause? extendsClause,
+    WithClause? withClause,
+    ImplementsClause? implementsClause,
+    MixinOnClause? mixinOnClause,
+    ExtensionOnClause? extensionOnClause,
+    NativeClause? nativeClause,
+  }) : _name = name,
+       _typeParameters = typeParameters,
+       _namePart = namePart,
+       _clauses = [
+         if (extendsClause case var clause?)
+           _Clause(clause.extendsKeyword, [clause.superclass]),
+         if (mixinOnClause case var clause?)
+           _Clause(clause.onKeyword, clause.superclassConstraints),
+         if (withClause case var clause?)
+           _Clause(clause.withKeyword, clause.mixinTypes),
+         if (implementsClause case var clause?)
+           _Clause(clause.implementsKeyword, clause.interfaces),
+         if (extensionOnClause case var clause?)
+           _Clause(clause.onKeyword, [clause.extendedType]),
+         if (nativeClause case var clause?)
+           _Clause(clause.nativeKeyword, [?clause.name]),
+       ],
+       _allowLeadingClause = extendsClause != null || mixinOnClause != null {
+    // Can have a name part or explicit name and type parameters, but not both.
+    assert(_name == null && _typeParameters == null || _namePart == null);
+  }
+
+  /// Builds a type whose body is an explicit brace-delimited list of [members].
+  void buildBlockBody(
+    Token leftBrace,
+    List<AstNode> members,
+    Token rightBrace,
+  ) {
+    Piece buildBody() => _visitor.pieces.build(() {
+      _visitor.writeBody(leftBrace, members, rightBrace);
+    });
+
+    _buildType(buildBody, TypeBodyType.block);
+  }
+
+  /// Builds a type whose [body] is a [ClassBody].
+  void buildClassBody(ClassBody body) {
+    Piece buildBody() => switch (body) {
+      BlockClassBody() => _visitor.pieces.build(() {
+        _visitor.writeBody(body.leftBracket, body.members, body.rightBracket);
+      }),
+      EmptyClassBody body => _visitor.pieces.tokenPiece(body.semicolon),
+    };
+
+    _buildType(buildBody, switch (body) {
+      BlockClassBody() => TypeBodyType.block,
+      EmptyClassBody() => TypeBodyType.semicolon,
+    });
+  }
+
+  /// Builds an enum type.
+  void buildEnum(EnumDeclaration node) {
+    Piece buildBody() {
+      // If the enum has any members we definitely need to force the body to
+      // split because there's a `;` in there. If it has a primary constructor,
+      // we could allow it on one line. But users generally wish enums were more
+      // eager to split and having the constructor and values all on one line
+      // is pretty hard to read:
+      //
+      //     enum E(final int x) { a(1), b(2) }
+      //
+      // So always force the body to split if there is a primary constructor.
+      if (node.body.members.isEmpty &&
+          node.namePart is! PrimaryConstructorDeclaration) {
+        return _buildNormalEnumBody(node.body);
+      } else {
+        return _buildEnhancedEnumBody(node.body);
+      }
+    }
+
+    _buildType(
+      buildBody,
+      node.body.members.isEmpty ? TypeBodyType.list : TypeBodyType.block,
+    );
+  }
+
+  /// Builds a mixin application class.
+  void buildMixinApplicationClass(
+    Token equals,
+    NamedType superclass,
+    Token semicolon,
+  ) {
+    _visitor.pieces.withMetadata(_metadata, () {
+      var header = _visitor.pieces.build(() {
+        _buildHeader();
+
+        // Mixin application classes have ` = Superclass` after the declaration
+        // name.
+        _visitor.pieces.space();
+        _visitor.pieces.token(equals);
+        _visitor.pieces.space();
+        _visitor.pieces.visit(superclass);
+      });
+
+      header = _buildClauses(header);
+
+      _visitor.pieces.add(
+        TypePiece(
+          header,
+          _visitor.tokenPiece(semicolon),
+          bodyType: TypeBodyType.semicolon,
+        ),
+      );
+    });
+  }
+
+  void _buildType(Piece Function() buildBody, TypeBodyType bodyType) {
+    _visitor.pieces.withMetadata(_metadata, () {
+      if (_namePart case PrimaryConstructorDeclaration constructor) {
+        var header = _visitor.pieces.build(() {
+          _buildHeader(includeParameters: false);
+        });
+
+        var parameters = _visitor.nodePiece(constructor.formalParameters);
+        var clauses = [for (var clause in _clauses) clause.build(_visitor)];
+
+        var bodyPiece = buildBody();
+
+        _visitor.pieces.add(
+          PrimaryTypePiece(header, parameters, clauses, bodyPiece, bodyType),
+        );
+      } else {
+        var header = _buildClauses(_visitor.pieces.build(_buildHeader));
+        var bodyPiece = buildBody();
+        _visitor.pieces.add(TypePiece(header, bodyPiece, bodyType: bodyType));
+      }
+    });
+  }
+
+  /// Writes the leading keywords, name, and type parameters for the type.
+  void _buildHeader({bool includeParameters = true}) {
+    var space = false;
+    for (var keyword in _keywords) {
+      if (space) _visitor.pieces.space();
+      _visitor.pieces.token(keyword);
+      if (keyword != null) space = true;
+    }
+
+    switch (_namePart) {
+      case null:
+        _visitor.pieces.token(_name, spaceBefore: space);
+        _visitor.pieces.visit(_typeParameters);
+
+      case NameWithTypeParameters(:var typeName, :var typeParameters):
+        _visitor.pieces.token(typeName, spaceBefore: space);
+        _visitor.pieces.visit(typeParameters);
+
+      case PrimaryConstructorDeclaration primary:
+        _visitor.pieces.token(primary.constKeyword, spaceBefore: space);
+        _visitor.pieces.token(primary.typeName, spaceBefore: true);
+        _visitor.pieces.visit(primary.typeParameters);
+        _visitor.pieces.visit(primary.constructorName);
+        if (includeParameters) _visitor.pieces.visit(primary.formalParameters);
+    }
+  }
+
+  /// If there are any clauses, wraps [header] in a [ClausePiece] for them.
+  Piece _buildClauses(Piece header) {
+    if (_clauses.isEmpty) return header;
+
+    return ClausePiece(header, [
+      for (var clause in _clauses) clause.build(_visitor),
+    ], allowLeadingClause: _allowLeadingClause);
+  }
+
+  /// Builds a [Piece] for the body of an enum declaration with values but not
+  /// members.
+  ///
+  /// Formats the constants like a list. This keeps the enum declaration on one
+  /// line if it fits.
+  Piece _buildNormalEnumBody(EnumBody body) {
+    var builder = DelimitedListBuilder(
+      _visitor,
+      const ListStyle(spaceWhenUnsplit: true),
+    );
+
+    builder.leftBracket(body.leftBracket);
+    body.constants.forEach(builder.visit);
+    builder.rightBracket(semicolon: body.semicolon, body.rightBracket);
+    return builder.build(
+      forceSplit: _visitor.style.preserveTrailingCommaBefore(
+        body.semicolon ?? body.rightBracket,
+      ),
+    );
+  }
+
+  /// Builds a [Piece] for the body of an enum declaration with members.
+  ///
+  /// Formats it like a block where each constant or member is on its own line.
+  Piece _buildEnhancedEnumBody(EnumBody body) {
+    // If there are members, format it like a block where each constant
+    // and member is on its own line.
+    var builder = SequenceBuilder(_visitor);
+    builder.leftBracket(body.leftBracket);
+
+    // In 3.10 and later, if the source has a trailing comma before the
+    // `;`, it is preserved and the `;` is put on its own line. If there
+    // is no trailing comma in the source, the `;` stays on the same line
+    // as the last constant. Prior to 3.10, the behavior is the same as
+    // when preserved trailing commas is off: the last constant's comma
+    // is removed and the `;` is placed there instead.
+    var preserveTrailingComma =
+        _visitor.style.preserveTrailingCommaAfterEnumValues &&
+        body.semicolon!.hasCommaBefore;
+    for (var constant in body.constants) {
+      var isLast = constant == body.constants.last;
+      builder.addCommentsBefore(constant.firstNonCommentToken);
+      builder.add(
+        _visitor.createEnumConstant(
+          constant,
+          commaAfter: !isLast || preserveTrailingComma,
+          semicolon: isLast ? body.semicolon : null,
+        ),
+      );
+    }
+
+    // If we are preserving the trailing comma, then put the `;` on its own line
+    // after the last constant.
+    if (preserveTrailingComma) {
+      builder.add(_visitor.tokenPiece(body.semicolon!));
+    }
+
+    // Insert a blank line between the constants and members.
+    builder.addBlank();
+
+    for (var member in body.members) {
+      builder.visit(member);
+
+      // If the node has a non-empty braced body, then require a blank
+      // line between it and the next node.
+      if (member.hasNonEmptyBody) builder.addBlank();
+    }
+
+    builder.rightBracket(body.rightBracket);
+    return builder.build();
+  }
+}
+
+/// A single `extends`, `with`, etc. clause that goes in a type header.
+final class _Clause {
+  final Token keyword;
+  final List<AstNode> types;
+
+  _Clause(this.keyword, this.types);
+
+  Piece build(PieceFactory visitor) => InfixPiece([
+    visitor.tokenPiece(keyword),
+    for (var type in types) visitor.nodePiece(type, commaAfter: true),
+  ], is3Dot7: visitor.style.is3Dot7);
+}
diff --git a/lib/src/piece/constructor.dart b/lib/src/piece/constructor.dart
index 3120320..e78cd3a 100644
--- a/lib/src/piece/constructor.dart
+++ b/lib/src/piece/constructor.dart
@@ -66,7 +66,10 @@
   final Piece _header;
 
   /// The constructor parameter list.
-  final Piece _parameters;
+  ///
+  /// Will be `null` if the constructor is a primary constructor initializer
+  /// body.
+  final Piece? _parameters;
 
   /// If this is a redirecting constructor, the redirection clause.
   final Piece? _redirect;
@@ -95,6 +98,19 @@
        _initializerSeparator = initializerSeparator,
        _initializers = initializers;
 
+  /// Creates a constructor piece for a primary constructor initializer body.
+  ConstructorPiece.thisBlock(
+    this._header,
+    this._body, {
+    Piece? initializerSeparator,
+    Piece? initializers,
+  }) : _parameters = null,
+       _canSplitParameters = false,
+       _hasOptionalParameter = false,
+       _redirect = null,
+       _initializerSeparator = initializerSeparator,
+       _initializers = initializers;
+
   @override
   List<State> get additionalStates => [
     if (_initializers != null) _splitBeforeInitializers,
@@ -105,23 +121,27 @@
   /// initializers may split.
   @override
   void applyConstraints(State state, Constrain constrain) {
-    // If there are no initializers, the parameters can do whatever.
-    if (_initializers case var initializers?) {
+    // If there are both parameters and initializers, constrain how they
+    // interact.
+    if ((_parameters, _initializers) case (
+      var parameters?,
+      var initializers?,
+    )) {
       switch (state) {
         case State.unsplit:
           // All parameters and initializers on one line.
-          constrain(_parameters, State.unsplit);
+          constrain(parameters, State.unsplit);
           constrain(initializers, State.unsplit);
 
         case _splitBeforeInitializers:
           // Only split before the `:` when the parameters fit on one line.
-          constrain(_parameters, State.unsplit);
+          constrain(parameters, State.unsplit);
           constrain(initializers, State.split);
 
         case _splitBetweenInitializers:
           // Split both the parameters and initializers and put the `) :` on
           // its own line.
-          constrain(_parameters, State.split);
+          constrain(parameters, State.split);
           constrain(initializers, State.split);
       }
     }
@@ -143,7 +163,7 @@
   @override
   void format(CodeWriter writer, State state) {
     writer.format(_header);
-    writer.format(_parameters);
+    if (_parameters case var parameters?) writer.format(parameters);
 
     if (_redirect case var redirect?) {
       writer.space();
@@ -175,7 +195,7 @@
   @override
   void forEachChild(void Function(Piece piece) callback) {
     callback(_header);
-    callback(_parameters);
+    if (_redirect case var parameters?) callback(parameters);
     if (_redirect case var redirect?) callback(redirect);
     if (_initializerSeparator case var separator?) callback(separator);
     if (_initializers case var initializers?) callback(initializers);
diff --git a/lib/src/piece/type.dart b/lib/src/piece/type.dart
index d32b91f..363e1bd 100644
--- a/lib/src/piece/type.dart
+++ b/lib/src/piece/type.dart
@@ -12,7 +12,7 @@
   /// other `extends`, `with`, etc. clauses.
   final Piece _header;
 
-  /// The `native` clause, if any, and the type body.
+  /// The type body.
   final Piece _body;
 
   /// What kind of body the type has.
@@ -55,6 +55,169 @@
   }
 }
 
+/// Piece for a type with a primary constructor (or extension type
+/// representation type) in the header.
+///
+/// These use a separate [Piece] class to handle the interactions between
+/// splitting in the constructor parameter list, and/or any subsequent clauses
+/// in the header. There are a few ways it can split with some constraints
+/// between them:
+///
+/// [State.unsplit] Everything in the header on one line:
+///
+///     class C(int x) extends S {
+///       body() {}
+///     }
+///
+/// [_beforeClauses] Split before every clause including the first. This is
+/// only allowed when the parameter list does not split:
+///
+///     class C(int x, int y)
+///         extends Super
+///         implements I {
+///       body() {}
+///     }
+///
+/// [_inlineClauses] The parameter list must split and the clauses all fit on
+/// one line between the `)` and the beginning of the body:
+///
+///     class C(
+///       int x,
+///       int y,
+///     ) extends S {
+///       body() {}
+///     }
+///
+/// [_betweenClauses] The parameter list must split and all but the first clause
+/// start their own lines:
+///
+///     class C(
+///       int x,
+///       int y,
+///     ) extends S
+///         implements I {
+///       body() {}
+///     }
+///
+/// [State.split] Similar to [_beforeClauses] but allows the parameter list to
+/// split too. Mainly so that if the constraints of the previous states can't
+/// otherwise be solved, then solver can still pick an invalid solution.
+///
+/// These constraints are designed to mostly avoid the clauses awkwardly
+/// hanging out on their own lines when the parameter splits as in:
+///
+///     class C(
+///       int x,
+///       int y,
+///     )
+///         extends S {
+///       body() {}
+///     }
+final class PrimaryTypePiece extends Piece {
+  /// Split before all clauses and don't allow the parameter list to split.
+  static const State _beforeClauses = State(1);
+
+  /// Keep all clauses inline and force the parameter list to split.
+  static const State _inlineClauses = State(2);
+
+  /// Split before every clause but the first.
+  static const State _betweenClauses = State(3);
+
+  /// The leading keywords and modifiers, type name, type parameters, and any
+  /// other `extends`, `with`, etc. clauses.
+  final Piece _header;
+
+  /// The constructor's formal parameter list.
+  final Piece _parameters;
+
+  /// The `extends`, `with`, `implements`, etc. clauses, if any.
+  final List<Piece> _clauses;
+
+  /// The type body.
+  final Piece _body;
+
+  /// What kind of body the type has.
+  final TypeBodyType _bodyType;
+
+  PrimaryTypePiece(
+    this._header,
+    this._parameters,
+    this._clauses,
+    this._body,
+    this._bodyType,
+  );
+
+  @override
+  List<State> get additionalStates => [
+    if (_clauses.isNotEmpty) ...[_beforeClauses, _inlineClauses],
+    _betweenClauses,
+    State.split,
+  ];
+
+  @override
+  Set<Shape> allowedChildShapes(State state, Piece child) {
+    if (child == _header) return Shape.all;
+
+    switch (state) {
+      case State.unsplit:
+        // We only restrict the header from splitting.
+        if (child != _body) return Shape.onlyInline;
+
+      case _beforeClauses:
+        // The parameters can't split and the clauses can.
+        if (child == _parameters) return Shape.onlyInline;
+
+      case _inlineClauses:
+        // The parameters must split and the clauses can't.
+        if (child == _parameters) return Shape.onlyBlock;
+        if (_clauses.contains(child)) return Shape.onlyInline;
+
+      case _betweenClauses:
+        // The parameters must split and the clauses can.
+        if (child == _parameters) return Shape.onlyBlock;
+    }
+
+    return Shape.all;
+  }
+
+  @override
+  void format(CodeWriter writer, State state) {
+    writer.format(_header);
+    writer.format(_parameters);
+
+    // Indent all of the clauses if any will start a line.
+    var indent =
+        state == _beforeClauses ||
+        state == _betweenClauses && _clauses.length > 1 ||
+        state == State.split;
+    if (indent) writer.pushIndent(Indent.infix);
+
+    for (var clause in _clauses) {
+      writer.splitIf(
+        state == _beforeClauses ||
+            state == _betweenClauses && clause != _clauses.first ||
+            state == State.split,
+      );
+      writer.format(clause);
+    }
+
+    if (indent) writer.popIndent();
+
+    if (_bodyType != TypeBodyType.semicolon) writer.space();
+    writer.format(_body);
+  }
+
+  @override
+  void forEachChild(void Function(Piece piece) callback) {
+    callback(_header);
+    callback(_parameters);
+    for (var clause in _clauses) {
+      callback(clause);
+    }
+    callback(_body);
+  }
+}
+
 /// What kind of body is used for the type.
 enum TypeBodyType {
   /// An always-split block body, as in a class declaration.
diff --git a/pubspec.yaml b/pubspec.yaml
index eb03d4d..e5cf351 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
 name: dart_style
 # Note: See tool/grind.dart for how to bump the version.
-version: 3.1.7
+version: 3.1.8-wip
 description: >-
   Opinionated, automatic Dart source code formatter.
   Provides an API and a CLI tool.
@@ -9,7 +9,7 @@
   sdk: ^3.10.0
 
 dependencies:
-  analyzer: '>=10.0.0 <12.0.0'
+  analyzer: '>=10.1.0 <12.0.0'
   args: ^2.5.0
   collection: ^1.19.0
   package_config: ^2.1.0
diff --git a/test/tall/declaration/class.unit b/test/tall/declaration/class.unit
index 504c7e9..1a3a3c7 100644
--- a/test/tall/declaration/class.unit
+++ b/test/tall/declaration/class.unit
@@ -1,11 +1,15 @@
 40 columns                              |
->>> Empty body.
+>>> Empty block body.
 class   A   {
 
 
 }
 <<<
 class A {}
+>>> (experiment primary-constructors) Empty semicolon body.
+class   A   ;
+<<< 3.13
+class A;
 >>> Don't split empty body.
 class LongClassNameWithExactLength____ {}
 <<<
@@ -74,3 +78,17 @@
 }
 <<<
 class SomeClass native "Zapp" {}
+>>> (experiment primary-constructors) Don't force blank lines around `;` bodies.
+class   A   ;
+class   B(int x)   ;
+class   C   ;
+class   D   {}
+class   E   ;
+<<< 3.13
+class A;
+class B(int x);
+class C;
+
+class D {}
+
+class E;
diff --git a/test/tall/declaration/class_comment.unit b/test/tall/declaration/class_comment.unit
index dffb66d..0438846 100644
--- a/test/tall/declaration/class_comment.unit
+++ b/test/tall/declaration/class_comment.unit
@@ -87,3 +87,9 @@
 /**
 */
 class Bar {}
+>>> (experiment primary-constructors) Comment before empty body.
+class C // Comment.
+;
+<<< 3.13
+class C // Comment.
+;
diff --git a/test/tall/declaration/constructor_factory.unit b/test/tall/declaration/constructor_factory.unit
new file mode 100644
index 0000000..a9b139c
--- /dev/null
+++ b/test/tall/declaration/constructor_factory.unit
@@ -0,0 +1,33 @@
+40 columns                              |
+(experiment primary-constructors)
+### Tests for using `factory` to define a constructor.
+>>> Factory.
+class C {
+  factory  (  int  x  )  {  }
+  factory  named  (  )  {  }
+}
+<<< 3.13
+class C {
+  factory(int x) {}
+  factory named() {}
+}
+>>> Const factory.
+class C {
+  const  factory  (  int  x  )  {  }
+  const  factory  named  (  )  {  }
+}
+<<< 3.13
+class C {
+  const factory(int x) {}
+  const factory named() {}
+}
+>>> Const redirecting factory.
+class C {
+  const  factory  (  int  x  )  =  C  .  named  ;
+  const  factory  named  (  )  =  C  ;
+}
+<<< 3.13
+class C {
+  const factory(int x) = C.named;
+  const factory named() = C;
+}
diff --git a/test/tall/declaration/constructor_new.unit b/test/tall/declaration/constructor_new.unit
new file mode 100644
index 0000000..3b4caff
--- /dev/null
+++ b/test/tall/declaration/constructor_new.unit
@@ -0,0 +1,77 @@
+40 columns                              |
+(experiment primary-constructors)
+### Tests for using `new` to define a constructor.
+>>> Generative.
+class C {
+  new  (  int  x  )  ;
+  new  named  (  )  {  }
+}
+<<< 3.13
+class C {
+  new(int x);
+  new named() {}
+}
+>>> External.
+class C {
+  external  new  (  int  x  )  ;
+  external  new  named  (  )  ;
+}
+<<< 3.13
+class C {
+  external new(int x);
+  external new named();
+}
+>>> Const generative.
+class C {
+  const  new  (  int  x  )  ;
+  const  new  named  (  )  {  }
+}
+<<< 3.13
+class C {
+  const new(int x);
+  const new named() {}
+}
+>>> Redirecting.
+class C {
+  new  (  int  x  )  :  this  .  named  (  )  ;
+  new  named  (  )  :  this  ( 1 )  ;
+}
+<<< 3.13
+class C {
+  new(int x) : this.named();
+  new named() : this(1);
+}
+>>> Const redirecting.
+class C {
+  const  new  (  int  x  )  :  this  .  named  (  )  ;
+  const  new  named  (  )  :  this  ( 1 )  ;
+}
+<<< 3.13
+class C {
+  const new(int x) : this.named();
+  const new named() : this(1);
+}
+>>> In enum.
+enum C {
+  a.named(), b(1);
+  const new  (  int  x  )  ;
+  const new  named  (  )  {  }
+}
+<<< 3.13
+enum C {
+  a.named(),
+  b(1);
+
+  const new(int x);
+  const new named() {}
+}
+>>> In extension type.
+extension type C._(int x) {
+  new  (  int  x  )  :  this._(x)  ;
+  const new  named  (  )  {  }
+}
+<<< 3.13
+extension type C._(int x) {
+  new(int x) : this._(x);
+  const new named() {}
+}
diff --git a/test/tall/declaration/constructor_parameter.unit b/test/tall/declaration/constructor_parameter.unit
index a2d745f..3134ec1 100644
--- a/test/tall/declaration/constructor_parameter.unit
+++ b/test/tall/declaration/constructor_parameter.unit
@@ -111,3 +111,66 @@
   int? _y;
   int? _z;
 }
+>>> (experiment primary-constructors) Declaring normal parameters.
+class C(var int x, final String y, var untyped, final another);
+<<< 3.13
+class C(
+  var int x,
+  final String y,
+  var untyped,
+  final another,
+);
+>>> (experiment primary-constructors) Declaring optional positional parameters.
+class C([var int? x, final String? y, var untyped, final another]);
+<<< 3.13
+class C([
+  var int? x,
+  final String? y,
+  var untyped,
+  final another,
+]);
+>>> (experiment primary-constructors) Declaring optional named parameters.
+class C({var int? x, final String? y, var untyped, final another});
+<<< 3.13
+class C({
+  var int? x,
+  final String? y,
+  var untyped,
+  final another,
+});
+>>> (experiment primary-constructors) Declaring required named parameters.
+class C({required var int? x, required final String? y, required var untyped, required final another});
+<<< 3.13
+class C({
+  required var int? x,
+  required final String? y,
+  required var untyped,
+  required final another,
+});
+>>> (experiment primary-constructors) Declaring coviariant parameters.
+class C(covariant var int? x, {required covariant var int z, covariant var String? a});
+<<< 3.13
+class C(
+  covariant var int? x, {
+  required covariant var int z,
+  covariant var String? a,
+});
+>>> (experiment primary-constructors) Declaring parameter with default values.
+class C1([var int x = 1, final String y = 's']);
+class C2({var int x = 1, final String y = 's'});
+<<< 3.13
+class C1([
+  var int x = 1,
+  final String y = 's',
+]);
+class C2({
+  var int x = 1,
+  final String y = 's',
+});
+>>> (experiment primary-constructors) Declaring function-typed parameters.
+class C(var int fn(), final callback(String s));
+<<< 3.13
+class C(
+  var int fn(),
+  final callback(String s),
+);
diff --git a/test/tall/declaration/constructor_primary.unit b/test/tall/declaration/constructor_primary.unit
new file mode 100644
index 0000000..2d7d6cc
--- /dev/null
+++ b/test/tall/declaration/constructor_primary.unit
@@ -0,0 +1,388 @@
+40 columns                              |
+(experiment primary-constructors)
+### Tests for primary constructors in the class header.
+>>> Unsplit.
+class  const  C  <  T  >  .  named  (  int  x  ,  int  y  )  {  }
+<<< 3.13
+class const C<T>.named(int x, int y) {}
+>>> Unsplit with clauses.
+class A(int x) extends S;
+
+class B(int x) with M1, M2;
+
+class C(int x) implements I1, I2;
+<<< 3.13
+class A(int x) extends S;
+
+class B(int x) with M1, M2;
+
+class C(int x) implements I1, I2;
+>>> Don't split after const.
+class const ExtremelyLongClassNameHere();
+<<<
+class const ExtremelyLongClassNameHere();
+>>> Don't split on class name.
+class LongClassName.longConstructorName();
+<<<
+class LongClassName.longConstructorName();
+>>> With everything split.
+class const C<T1 extends TypeBound, T2 extends TypeBound>.named
+(final int parameter1, {required var String parameter2})
+extends Superclass<TypeArgument1, TypeArgument2>
+with Mixin1, Mixin2
+implements Interface1, Interface2, Interface3 { void body() {} }
+<<< 3.13
+class const C<
+  T1 extends TypeBound,
+  T2 extends TypeBound
+>.named(
+  final int parameter1, {
+  required var String parameter2,
+}) extends
+        Superclass<
+          TypeArgument1,
+          TypeArgument2
+        >
+    with Mixin1, Mixin2
+    implements
+        Interface1,
+        Interface2,
+        Interface3 {
+  void body() {}
+}
+>>> On enum type with everything.
+enum const C<T1 extends TypeBound, T2 extends TypeBound>.named
+(final int parameter1, {required var String parameter2})
+with Mixin1, Mixin2
+implements Interface1, Interface2, Interface3 {
+a.named(1, parameter2: 'a'),
+b.named(2, parameter2: 'd'),
+c.named(3, parameter2: 'c');
+void body() {} }
+<<< 3.13
+enum const C<
+  T1 extends TypeBound,
+  T2 extends TypeBound
+>.named(
+  final int parameter1, {
+  required var String parameter2,
+}) with Mixin1, Mixin2
+    implements
+        Interface1,
+        Interface2,
+        Interface3 {
+  a.named(1, parameter2: 'a'),
+  b.named(2, parameter2: 'd'),
+  c.named(3, parameter2: 'c');
+
+  void body() {}
+}
+>>> Enums always split if there is a primary constructor.
+enum const C(int x) { a, b, c }
+<<< 3.13
+enum const C(int x) {
+  a,
+  b,
+  c
+}
+>>> With optional parameters.
+class Foo(int a, int b, [int? c, int? d]);
+<<< 3.13
+class Foo(
+  int a,
+  int b, [
+  int? c,
+  int? d,
+]);
+>>> With named parameters.
+class Foo(int a, int b, {int? c, required int d});
+<<< 3.13
+class Foo(
+  int a,
+  int b, {
+  int? c,
+  required int d,
+});
+>>> Prefer splitting parameters instead of type parameters.
+class C<Type1, Type2>(int param1, int param2) {}
+<<< 3.13
+class C<Type1, Type2>(
+  int param1,
+  int param2,
+) {}
+>>> Prefer splitting before `extends` instead of parameters.
+class C(int param1, int param2) extends S {}
+<<< 3.13
+class C(int param1, int param2)
+    extends S {}
+>>> Prefer splitting parameters instead of inside `extends` clause.
+class C(int param1, int param2)
+extends Superclass<TypeArg1,TypeArg2> {}
+<<< 3.13
+class C(
+  int param1,
+  int param2,
+) extends
+    Superclass<TypeArg1, TypeArg2> {}
+>>> Prefer splitting before `with` instead of parameters.
+class C(int param1, int param2) with M {}
+<<< 3.13
+class C(int param1, int param2)
+    with M {}
+>>> Prefer splitting inside `with` clause instead of parameters.
+class C(int param1, int param2) with Mixin1, Mixin2, Mixin3, Mixin4 {}
+<<< 3.13
+class C(int param1, int param2)
+    with
+        Mixin1,
+        Mixin2,
+        Mixin3,
+        Mixin4 {}
+>>> Prefer splitting before `implements` instead of parameters.
+class C(int param1, int param2) implements I {}
+<<< 3.13
+class C(int param1, int param2)
+    implements I {}
+>>> Prefer splitting inside `implements` clause instead of parameters.
+class C(int param1, int param2) implements Interface1, Interface2, Interface3 {}
+<<< 3.13
+class C(int param1, int param2)
+    implements
+        Interface1,
+        Interface2,
+        Interface3 {}
+>>> Split parameter list with split type parameters.
+class Class<TypeParameter1, TypeParameter2>
+(var int parameter1, final String parameter2);
+<<< 3.13
+class Class<
+  TypeParameter1,
+  TypeParameter2
+>(
+  var int parameter1,
+  final String parameter2,
+);
+>>> Split parameter list with unsplit `extends` clause.
+class C(var int parameter1, final String parameter2) extends S<int>
+{ body() {} }
+<<< 3.13
+class C(
+  var int parameter1,
+  final String parameter2,
+) extends S<int> {
+  body() {}
+}
+>>> Split parameter list with split `extends` clause.
+class C(var int parameter1,final String parameter2)
+extends Superclass<TypeArgument1, TypeArgument2>
+{ body() {} }
+<<< 3.13
+class C(
+  var int parameter1,
+  final String parameter2,
+) extends
+    Superclass<
+      TypeArgument1,
+      TypeArgument2
+    > {
+  body() {}
+}
+>>> Split parameter list with unsplit `with` clause.
+class C(var int parameter1, final String parameter2) with M<int>, Mixin2
+{ body() {} }
+<<< 3.13
+class C(
+  var int parameter1,
+  final String parameter2,
+) with M<int>, Mixin2 {
+  body() {}
+}
+>>> Split parameter list with split `with` clause.
+class C(var int parameter1,final String parameter2)
+with Mixin1, Mixin2, Mixin3, Mixin4, Mixin5
+{ body() {} }
+<<< 3.13
+class C(
+  var int parameter1,
+  final String parameter2,
+) with
+    Mixin1,
+    Mixin2,
+    Mixin3,
+    Mixin4,
+    Mixin5 {
+  body() {}
+}
+>>> Split parameter list with unsplit `implements` clause.
+class C(var int parameter1, final String parameter2) implements I<int>, I2
+{ body() {} }
+<<< 3.13
+class C(
+  var int parameter1,
+  final String parameter2,
+) implements I<int>, I2 {
+  body() {}
+}
+>>> Split parameter list with split `implements` clause.
+class C(var int parameter1,final String parameter2)
+implements Interface1, Interface2, Interface3
+{ body() {} }
+<<< 3.13
+class C(
+  var int parameter1,
+  final String parameter2,
+) implements
+    Interface1,
+    Interface2,
+    Interface3 {
+  body() {}
+}
+>>> Split parameter list with multiple unsplit `implements` clauses.
+class C(var int parameter1, final String parameter2) extends S with M implements I
+{ body() {} }
+<<< 3.13
+class C(
+  var int parameter1,
+  final String parameter2,
+) extends S with M implements I {
+  body() {}
+}
+>>> Split parameter list with multiple split clauses.
+class C(var int parameter1,final String parameter2)
+extends Superclass
+with Mixin1, Mixin2, Mixin3
+implements Interface1, Interface2, Interface3
+{ body() {} }
+<<< 3.13
+class C(
+  var int parameter1,
+  final String parameter2,
+) extends Superclass
+    with Mixin1, Mixin2, Mixin3
+    implements
+        Interface1,
+        Interface2,
+        Interface3 {
+  body() {}
+}
+>>> Split type parameters, empty parameter list, extends clause.
+class C<TypeParam1, TypeParam2, TypeParam3>() extends Superclass { body() {} }
+<<< 3.13
+class C<
+  TypeParam1,
+  TypeParam2,
+  TypeParam3
+>() extends Superclass {
+  body() {}
+}
+>>> Split type parameters, unsplit parameter list, extends clause.
+class C<TypeParam1, TypeParam2, TypeParam3>(int x, int y)
+extends Superclass { body() {} }
+<<< 3.13
+class C<
+  TypeParam1,
+  TypeParam2,
+  TypeParam3
+>(int x, int y) extends Superclass {
+  body() {}
+}
+>>> Split type parameters, empty parameter list, with clause.
+class C<TypeParam1, TypeParam2, TypeParam3>() with Mixin1, Mixin2 { body() {} }
+<<< 3.13
+class C<
+  TypeParam1,
+  TypeParam2,
+  TypeParam3
+>() with Mixin1, Mixin2 {
+  body() {}
+}
+>>> Split type parameters, unsplit parameter list, with clause.
+class C<TypeParam1, TypeParam2, TypeParam3>(int x, int y)
+with Mixin1, Mixin2 { body() {} }
+<<< 3.13
+class C<
+  TypeParam1,
+  TypeParam2,
+  TypeParam3
+>(int x, int y) with Mixin1, Mixin2 {
+  body() {}
+}
+>>> Split type parameters, empty parameter list, implements clause.
+class C<TypeParam1, TypeParam2, TypeParam3>() implements Interface1, Interface2 { body() {} }
+<<< 3.13
+class C<
+  TypeParam1,
+  TypeParam2,
+  TypeParam3
+>() implements Interface1, Interface2 {
+  body() {}
+}
+>>> Split type parameters, unsplit parameter list, implements clause.
+class C<TypeParam1, TypeParam2, TypeParam3>(int x, int y)
+implements I1, I2 { body() {} }
+<<< 3.13
+class C<
+  TypeParam1,
+  TypeParam2,
+  TypeParam3
+>(int x, int y) implements I1, I2 {
+  body() {}
+}
+>>> Split type parameters, empty parameter list, multiple unsplit clauses.
+class C<TypeParam1, TypeParam2, TypeParam3>()
+extends S with M1 implements I1 { body() {} }
+<<< 3.13
+class C<
+  TypeParam1,
+  TypeParam2,
+  TypeParam3
+>() extends S with M1 implements I1 {
+  body() {}
+}
+>>> Split type parameters, unsplit parameter list, multiple unsplit clauses.
+class C<TypeParam1, TypeParam2, TypeParam3>(s)
+extends S with M implements I { body() {} }
+<<< 3.13
+class C<
+  TypeParam1,
+  TypeParam2,
+  TypeParam3
+>(s) extends S with M implements I {
+  body() {}
+}
+>>> Split type parameters, empty parameter list, multiple split clauses.
+class C<TypeParam1, TypeParam2, TypeParam3>()
+extends Superclass with Mixin1, Mixin2, Mixin3
+implements Interface1, Interface2, Interface3 { body() {} }
+<<< 3.13
+class C<
+  TypeParam1,
+  TypeParam2,
+  TypeParam3
+>()
+    extends Superclass
+    with Mixin1, Mixin2, Mixin3
+    implements
+        Interface1,
+        Interface2,
+        Interface3 {
+  body() {}
+}
+>>> Split type parameters, unsplit parameter list, multiple split clauses.
+class C<TypeParam1, TypeParam2, TypeParam3>(int x, int y)
+extends Superclass with Mixin1, Mixin2, Mixin3
+implements Interface1, Interface2, Interface3 { body() {} }
+<<< 3.13
+class C<
+  TypeParam1,
+  TypeParam2,
+  TypeParam3
+>(int x, int y)
+    extends Superclass
+    with Mixin1, Mixin2, Mixin3
+    implements
+        Interface1,
+        Interface2,
+        Interface3 {
+  body() {}
+}
diff --git a/test/tall/declaration/extension_type.unit b/test/tall/declaration/extension_type.unit
index 0386813..579052a 100644
--- a/test/tall/declaration/extension_type.unit
+++ b/test/tall/declaration/extension_type.unit
@@ -4,6 +4,10 @@
   }
 <<<
 extension type A(int a) {}
+>>> (experiment primary-constructors) Empty semicolon body.
+extension type   A(int a)   ;
+<<< 3.13
+extension type A(int a);
 >>> Const and named constructor.
 extension  type  const  A  .  name  (  int  a  ) {}
 <<<
@@ -48,8 +52,7 @@
 <<<
 extension type LongExtensionType(
   LongTypeName a
-)
-    implements Something {
+) implements Something {
   method() {
     ;
   }
@@ -128,3 +131,44 @@
   int me(int x) => x;
   int operator +(int x) => x;
 }
+>>> Force a blank line before and after an extension type declaration.
+var x = 1; extension type A(int x) {} var y = 2;
+
+
+extension type B(int x) {}
+
+
+
+var z = 3;
+<<< 3.7
+var x = 1;
+extension type A(int x) {}
+var y = 2;
+
+extension type B(int x) {}
+
+var z = 3;
+<<< 3.13
+var x = 1;
+
+extension type A(int x) {}
+
+var y = 2;
+
+extension type B(int x) {}
+
+var z = 3;
+>>> (experiment primary-constructors) Don't force blank lines around `;` bodies.
+extension type   A(int x)   ;
+extension type   B(int x)   ;
+extension type   C(int x)   ;
+extension type   D(int x)   {}
+extension type   E(int x)   ;
+<<< 3.13
+extension type A(int x);
+extension type B(int x);
+extension type C(int x);
+
+extension type D(int x) {}
+
+extension type E(int x);
diff --git a/test/tall/declaration/extension_type_comment.unit b/test/tall/declaration/extension_type_comment.unit
index 33240ad..0c34468 100644
--- a/test/tall/declaration/extension_type_comment.unit
+++ b/test/tall/declaration/extension_type_comment.unit
@@ -3,11 +3,22 @@
 /*a*/ extension /*b*/ type /*c*/ A
 /*d*/ ( /*e*/ @ /*f*/ override /*g*/ int /*h*/ a /*i*/ ) /*j*/
 implements /*k*/ I1 /*l*/ , /*m*/ I2 /*n*/ { /*o*/ } /*p*/
-<<<
+<<< 3.7
 /*a*/
 extension /*b*/ type /*c*/ A
-/*d*/ ( /*e*/
-  @ /*f*/ override /*g*/
+/*d*/ (
+  /*e*/ @ /*f*/ override /*g*/
+  int /*h*/ a /*i*/
+) /*j*/ implements /*k*/
+    I1 /*l*/, /*m*/
+    I2 /*n*/ {
+  /*o*/
+} /*p*/
+<<< 3.8
+/*a*/
+extension /*b*/ type /*c*/ A
+/*d*/ (
+  /*e*/ @ /*f*/ override /*g*/
   int /*h*/ a /*i*/
 ) /*j*/
     implements /*k*/
@@ -37,7 +48,7 @@
 I // r
 { // s
 } // t
-<<<
+<<< 3.7
 // 0
 @patch // a
 extension // b
@@ -50,7 +61,33 @@
 > // h
 . // i
 name // j
-( // k
+(
+  // k
+  @ // l
+  required // m
+  int // n
+  a // o
+) // p
+implements // q
+    I // r
+    {
+  // s
+} // t
+<<< 3.8
+// 0
+@patch // a
+extension // b
+type // c
+const // d
+A // e
+<
+  // f
+  T // g
+> // h
+. // i
+name // j
+(
+  // k
   @ // l
   required // m
   int // n
@@ -61,3 +98,9 @@
         {
   // s
 } // t
+>>> (experiment primary-constructors) Comment before empty body.
+extension type I(int i) // Comment.
+;
+<<< 3.13
+extension type I(int i) // Comment.
+;
diff --git a/test/tall/declaration/mixin.unit b/test/tall/declaration/mixin.unit
index b5c2b68..0a3b769 100644
--- a/test/tall/declaration/mixin.unit
+++ b/test/tall/declaration/mixin.unit
@@ -52,3 +52,30 @@
 abstract base mixin class M4 {}
 
 base mixin M5 {}
+>>> Force a blank line before and after a mixin declaration.
+var x = 1; mixin M1 {} var y = 2;
+
+
+mixin M2 {}
+
+
+
+var z = 3;
+<<< 3.7
+var x = 1;
+mixin M1 {}
+var y = 2;
+
+mixin M2 {}
+
+var z = 3;
+<<< 3.13
+var x = 1;
+
+mixin M1 {}
+
+var y = 2;
+
+mixin M2 {}
+
+var z = 3;
diff --git a/test/tall/declaration/this_block.unit b/test/tall/declaration/this_block.unit
new file mode 100644
index 0000000..126df16
--- /dev/null
+++ b/test/tall/declaration/this_block.unit
@@ -0,0 +1,119 @@
+40 columns                              |
+(experiment primary-constructors)
+### Tests for a primary constructor `this` block in the class body.
+>>> Unsplit.
+class C(int x) {
+  int y;
+  this : y = x, assert(x != 0);
+}
+<<< 3.13
+class C(int x) {
+  int y;
+  this : y = x, assert(x != 0);
+}
+>>> Unnamed super constructor.
+class Foo(a, b) {
+  this  :  super  (  a  ,  other  :  b  )  ;
+}
+<<< 3.13
+class Foo(a, b) {
+  this : super(a, other: b);
+}
+>>> Named super constructor.
+class Foo(a, b) {
+  this  :  super  .  name  (  a  ,  other  :  b  )  ;
+}
+<<< 3.13
+class Foo(a, b) {
+  this : super.name(a, other: b);
+}
+>>> Split a single initializer.
+class MyClass() {
+  this : super(firstArgument, secondArg);
+}
+<<< 3.13
+class MyClass() {
+  this
+    : super(firstArgument, secondArg);
+}
+>>> Unsplit field initializers.
+class X() {
+  var x, y;
+  this : x = 1, y = 2;
+}
+<<< 3.13
+class X() {
+  var x, y;
+  this : x = 1, y = 2;
+}
+>>> Split all field initializers.
+class MyClass() {
+  this : first = "some value", second = "another",
+        third = "last";
+}
+<<< 3.13
+class MyClass() {
+  this
+    : first = "some value",
+      second = "another",
+      third = "last";
+}
+>>> Wrap split initializers past the `:`.
+class Foo() {
+  this
+      : initializer = function(argument, argument),
+        initializer2 = function(argument, argument);
+}
+<<< 3.13
+class Foo() {
+  this
+    : initializer = function(
+        argument,
+        argument,
+      ),
+      initializer2 = function(
+        argument,
+        argument,
+      );
+}
+>>> Empty block body.
+class Foo() {
+  this  {  }
+}
+<<< 3.13
+class Foo() {
+  this {}
+}
+>>> Non-empty block body.
+class Foo() {
+  this  {  body();  }
+}
+<<< 3.13
+class Foo() {
+  this {
+    body();
+  }
+}
+>>> Block body with unsplit initializers.
+class Foo() {
+  this  :  a = 1,  b = 2,  c = 3   {  body();  }
+}
+<<< 3.13
+class Foo() {
+  this : a = 1, b = 2, c = 3 {
+    body();
+  }
+}
+>>> Block body with split initializers.
+class Foo() {
+  this  :  fieldA = 1,  fieldB = 2,  fieldC = 3   {  body();  }
+}
+<<< 3.13
+class Foo() {
+  this
+    : fieldA = 1,
+      fieldB = 2,
+      fieldC = 3 {
+    body();
+  }
+}
diff --git a/test/tall/preserve_trailing_commas/constructor.unit b/test/tall/preserve_trailing_commas/constructor.unit
index f22d712..3c66d3e 100644
--- a/test/tall/preserve_trailing_commas/constructor.unit
+++ b/test/tall/preserve_trailing_commas/constructor.unit
@@ -78,3 +78,21 @@
   A(int x) : this.named(x);
   A.named(int x) : this(x);
 }
+>>> (experiment primary-constructors) Primary constructor parameter list splits with trailing comma.
+class A(int x,);
+<<<
+class A(
+  int x,
+);
+>>> (experiment primary-constructors) Doesn't force split primary constructor parameter list without trailing comma.
+class A(int x, int y);
+<<< 3.11
+class A(int x, int y);
+>>> (experiment primary-constructors) May still split primary constructor without trailing comma if doesn't fit.
+class A(int parameter1, int parameter2, int parameter3);
+<<< 3.11
+class A(
+  int parameter1,
+  int parameter2,
+  int parameter3,
+);
diff --git a/test/tall/regression/1500/1505.unit b/test/tall/regression/1500/1505.unit
index d90cb83..af00567 100644
--- a/test/tall/regression/1500/1505.unit
+++ b/test/tall/regression/1500/1505.unit
@@ -5,5 +5,4 @@
 <<<
 extension type JSExportedDartFunction._(
   JSExportedDartFunctionRepType _jsExportedDartFunction
-)
-    implements JSFunction {}
+) implements JSFunction {}