Swift Marshal

Every Swift type tells its story in some order. When each one picks its own, every file becomes a small search and every review has an opinion on where the new property goes. swift-marshal makes the order a rule: written once in a small YAML file, checked in CI, and fixed with one command.

Install
brew tap ericodx/homebrew-tools
brew install swift-marshal
10 member kinds, ordered by one YAML list
5 type kinds: struct, class, enum, actor, protocol
2 modes: check reports, fix rewrites
0 dependencies to add to your project

Before & after

The same type, as written and after swift-marshal fix. Initializers and type-level state come first, instance state next, behavior last. The doc comments on name and api moved with them, and name, api and id keep their order among themselves.

Before
struct UserSession {
    typealias ID = UUID

    func refresh() async throws {
        let snapshot = try await api.fetch(id: id)
        apply(snapshot)
    }

    /// The signed-in user's display name.
    var name: String

    enum State {
        case active, expired, revoked
    }

    static let anonymous = UserSession(id: ID(), api: .stub)

    private func apply(_ snapshot: Snapshot) {
        name = snapshot.name
    }

    /// Backing service for remote calls.
    private let api: UserAPI
    let id: ID

    init(id: ID, api: UserAPI) {
        self.id = id
        self.api = api
    }

    static func current() -> UserSession {
        anonymous
    }
}
After
struct UserSession {
    typealias ID = UUID

    init(id: ID, api: UserAPI) {
        self.id = id
        self.api = api
    }

    static let anonymous = UserSession(id: ID(), api: .stub)

    /// The signed-in user's display name.
    var name: String

    /// Backing service for remote calls.
    private let api: UserAPI
    let id: ID

    enum State {
        case active, expired, revoked
    }

    static func current() -> UserSession {
        anonymous
    }

    func refresh() async throws {
        let snapshot = try await api.fetch(id: id)
        apply(snapshot)
    }

    private func apply(_ snapshot: Snapshot) {
        name = snapshot.name
    }
}

Once every type follows the same order, reading a file becomes orientation rather than discovery: what a type requires, what it holds, what it does. Reviews stop debating where a declaration belongs, and two people adding members to the same type land them in predictable places.

Your order, written down

The order is a list in a YAML file you commit with the code. Start from the default, or make rules as specific as your team’s conventions: by visibility, by static or instance, by whether a property carries an attribute.

Default
# .swift-marshal.yaml — the default written by init
version: 1

ordering:
  members:
    - typealias
    - associatedtype
    - initializer
    - type_property
    - instance_property
    - subtype
    - type_method
    - instance_method
    - subscript
    - deinitializer
More specific rules
ordering:
  members:
    - initializer
    - property:
        visibility: public
    - property:
        annotated: true
    - instance_property
    - method:
        kind: static
    - instance_method

Rules are read top to bottom and a member takes the first one that matches, so specific rules go above the general ones. See the configuration reference.

How it works

check and fix share one pipeline and part ways only at the last step, so what check reports is exactly what fix will do.

  1. 01 Parse SwiftSyntax builds the real syntax tree of every file — no regular expressions over source text. Files are processed concurrently.
  2. 02 Classify Every struct, class, enum, actor and protocol is found, at any nesting depth, and each member gets a kind and a visibility.
  3. 03 Plan Each member goes to the first rule in your config that matches it. The sort is stable: members of the same kind keep the order you wrote them in.
  4. 04 Report or rewrite check prints the current and target order of every type that is out of place. fix moves the syntax nodes themselves and writes the file back.

Safe to run on everything

A tool that rewrites source files has to earn trust before anyone runs it on a whole codebase. swift-marshal only changes where declarations sit — never what they say.

Look before you write check never touches a file. fix --dry-run names every file that would change and exits 1, so CI can ask for a fix without making one.
Same-kind order is kept Stored properties stay in the order you wrote them, so the memberwise initializer Swift synthesizes from them keeps its parameters.
Declarations move whole A member moves as one syntax node, attributes and body together. Enum cases, and anything else it does not classify, stay exactly where they are.
Done means done Run check after fix and every type reports [order ok]. The rewrite and the rules agree, so CI stays green after the fix lands.

Built for CI

A convention that lives in a wiki drifts. One that runs on every pull request doesn’t.

Commands
# Fail the job when a type is out of order
swift-marshal check --path Sources/

# Ask pull requests to run fix, without changing anything in CI
swift-marshal fix --dry-run --path Sources/
Dry-run output
Would reorder: Sources/App/UserSession.swift
⚠ 1 file would be modified
Clear exit codes 0 when every type is in order, 1 for violations or pending changes, 2 for real errors — so a broken path never looks like a style problem.
pre-commit hook Block a commit that introduces a type out of order, before it reaches review. Setup is a few lines in .pre-commit-config.yaml.
Warn before you enforce --warn-only reports violations and exits 0, for the weeks while a codebase is being brought in line.
One config, many rules Point --config at a stricter file for CI than the one developers use locally, or keep one per package in a monorepo.
CI recipes

Three commands to a tidy codebase

Run check first to see the size of the change, then fix it in one commit — a pure reordering diff that’s easy to review on its own.

Quick start
# Detects Package.swift or .xcodeproj and writes .swift-marshal.yaml
swift-marshal init

# See what is out of order
swift-marshal check

# Fix it
swift-marshal fix
check output
Sources/App/UserSession.swift:
  structType UserSession (line 1)
    [needs reordering]
    original:
      - typealias ID
      - instance_method refresh
      - instance_property name
      - subtype State
      - type_property anonymous
      - instance_method apply
      - instance_property api
      - instance_property id
      - initializer init
      - type_method current
    reordered:
      - typealias ID
      - initializer init
      - type_property anonymous
      - instance_property name
      - instance_property api
      - instance_property id
      - subtype State
      - type_method current
      - instance_method refresh
      - instance_method apply
  enumType State (line 12)
    (no members)

Summary: 2 types, 1 need reordering

✗ 1 type in 1 file needs reordering
  Run 'swift-marshal fix' to apply changes

Inside Xcode

Two plugins, no global install needed. The build plugin runs check on every build and shows a warning on each type that is out of order. The command plugin runs fix when you ask for it — from the terminal, or from Product → SwiftMarshal in Xcode.

Command plugin
# Fix every target in the package
swift package marshal

# Or just one
swift package marshal --target MyTarget
Build plugin warning
Sources/App/UserSession.swift:1: warning: 'UserSession' members need reordering

Also included

init that knows your project Package.swift sets paths to Sources/, an .xcodeproj to your app folder. Edit the file only if you want to.
Quiet and Xcode output --quiet prints only the files that need work; --xcode prints one warning per type for build phases.
Files or folders Check one file, a list of files, a folder with --path, or the paths in your config — whichever the moment needs.

Free and open source

MIT-licensed, no account, no telemetry. It runs on your machine and your CI runners, and your code never leaves them. Issues, ideas and pull requests are welcome.

↑ Back to top