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.
brew tap ericodx/homebrew-tools brew install swift-marshal
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.
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
}
} 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.
# .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 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.
- 01 Parse SwiftSyntax builds the real syntax tree of every file — no regular expressions over source text. Files are processed concurrently.
- 02 Classify Every struct, class, enum, actor and protocol is found, at any nesting depth, and each member gets a kind and a visibility.
- 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.
- 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.
Built for CI
A convention that lives in a wiki drifts. One that runs on every pull request doesn’t.
# 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/
Would reorder: Sources/App/UserSession.swift ⚠ 1 file would be modified
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.
# 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
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.
# Fix every target in the package swift package marshal # Or just one swift package marshal --target MyTarget
Sources/App/UserSession.swift:1: warning: 'UserSession' members need reordering
Also included
From the blog
Notes from building it — the bugs, the measurements, and the design decisions.
Documentation
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.