|

swift-format: From Syntax Trees to Readable Code

Swift syntax elements arranged into lines of code

A formatter looks simple from the outside. You give it a Swift file, and it gives you back the same code with consistent spacing and line breaks. In a team, that can be enough reason to use one: nobody needs to discuss where a brace belongs during a code review.

But formatting becomes more interesting when we ask what the tool is actually allowed to change. Consider two statements on one line, separated by a semicolon. Removing the extra punctuation is a style decision, but it also means the formatter has to separate the statements without losing either of them. Now add a comment after the semicolon, or put those statements inside a closure. A search-and-replace operation is no longer a useful model for the job.

swift-format handles this by working with Swift syntax. Its implementation separates three tasks: understand the structure of the source, apply rules that may rewrite that structure, and decide how to print it. The linter uses some of the same machinery, but it answers a different question: what should the developer fix without rewriting the file?

That division is worth exploring, especially if you are interested in code generation, developer tools, or the trade-offs behind an automated style policy.

When a style rule changes the syntax tree

Here is a small example. It is ordinary Swift, and the two calls are intentionally placed on one line.

func report() {
  print("Started"); print("Finished")
}

With the default DoNotUseSemicolons rule enabled, formatting removes the separator and places the second statement on a new line:

func report() {
  print("Started")
  print("Finished")
}

This is more than choosing between a space and a newline. The parser represents the statements as syntax nodes, and the rule can remove the semicolon token from the appropriate node. If another statement follows on the same line, the rule also arranges for a line break before it. The pretty printer can then produce normal indentation.

The implementation lives in Sources/SwiftFormat/Rules/DoNotUseSemicolons.swift. One detail I like is how carefully it treats comments. A line comment should stay with the relevant statement. A block comment between two statements may need to move with the next one instead. The rule carries trivia, SwiftSyntax’s representation of whitespace and comments, while it changes the statement list.

It also has a specific exception for the semicolon separating a do statement from a following while statement. That is a useful warning against a broad claim such as “the formatter deletes every semicolon.” The intention is to enforce a style without breaking syntax that needs special handling.

The corresponding DoNotUseSemicolonsTests.swift covers statements, nested blocks, comments, and that exception. Reading the rule and its tests together gives a clearer picture than simply looking at a formatted result.

The public interface is smaller than the machinery

For most projects, this work is exposed through two command-line operations. Starting with Swift 6, the Swift toolchain includes the tool under the swift format command. A separately installed executable is named swift-format.

# Print the formatted result without modifying the file.
swift format format Sources/Reporter.swift

# Apply changes to the source file.
swift format format --in-place Sources/Reporter.swift

# Report violations and fail CI if warnings are found.
swift format lint --strict Sources/Reporter.swift

The distinction between the last two commands matters. Formatting is a write operation when --in-place is present, and the documentation says it does not create a backup. Linting reports findings instead. With --strict, warnings cause a nonzero exit code, which makes the lint command suitable for a CI check.

There are also two SwiftPM command plugins, FormatPlugin and LintPlugin, and the SwiftFormat library is a separate package product. You can use the CLI without embedding the library, but the library is useful when another tool needs formatting as one step in its own workflow.

For example, this uses the public API to format source held in memory:

import SwiftFormat

var output = ""
let formatter = SwiftFormatter(configuration: Configuration())

try formatter.format(
  source: "func report(){print(\"Started\");print(\"Finished\")}",
  assumingFileURL: nil,
  selection: .infinite,
  to: &output
)

print(output)

The important part is not the few lines of setup. SwiftFormatter takes a Configuration, so the same formatting policy can be used by a command-line tool, an editor integration, or a code generator. Its syntax-tree overloads are useful to generators that already build SourceFileSyntax values. Those callers must supply an appropriately folded tree and an operator table, however; the overload is not a shortcut around all parser requirements.

Parsing is the beginning, not the formatting

The first substantial step for a source string is parseAndEmitDiagnostics in Sources/SwiftFormat/Core/Parsing.swift. It uses SwiftParser to build a SourceFileSyntax, and SwiftOperators to fold sequence expressions according to an operator table. The public string-based formatter uses the standard operator table, while the lower-level API allows callers to provide one for custom operators.

That detail is easy to miss. An expression containing several operators may not yet have the tree shape that a syntax-aware formatting rule expects. Operator folding gives later stages a more useful structure. It is also why the syntax-tree API documents that the input must already have its expressions folded.

Parsing here is not the same as compiling. The formatter can reason about declarations, expressions, comments, and delimiters without resolving your application’s types or running its code. It also does not promise to catch every bug: parsing and style checks are different from type checking or tests.

After parsing, SwiftFormatter creates a Context carrying configuration, source information, selection, and the finding callback. It passes the tree to FormatPipeline, which applies enabled syntax rules and returns a transformed tree. Only after that does the formatter create a PrettyPrinter.

The flow, in simplified form, is:

Swift source
    |
SwiftParser + operator folding
    |
SourceFileSyntax
    |
FormatPipeline: syntax rewrites
    |
TokenStreamCreator: layout instructions
    |
PrettyPrinter: scan and print
    |
Formatted source

This is a useful separation. The semicolon rule does not need to decide how wide a line should be. It changes the syntax. The component that prints the file decides how those syntax nodes fit into lines under the selected configuration.

Why the pretty printer needs its own language

Once syntax rewriting is finished, the formatter still has a difficult job. A function call can fit on one line until an argument becomes too long. A collection can wrap across multiple lines while keeping its commas and closing bracket readable. Comments and existing line breaks can affect what a sensible layout looks like.

The PrettyPrinter is not handed a string and asked to insert newlines blindly. TokenStreamCreator, a visitor over SwiftSyntax nodes, builds a stream of formatting tokens. Alongside the source tokens, the stream includes instructions such as spaces, optional breaks, group openings and closings, and comments.

Think of a group around a parameter list. The printer can keep the group together when it fits. When it does not, the break instructions tell it where splitting is allowed and how the following lines should be indented. GroupBreakStyle distinguishes consistent groups, whose breaks move together when the group wraps, from inconsistent groups, which may break only where necessary.

The design document, Documentation/PrettyPrinter.md, describes two further stages. A scan computes lengths for the token stream, including groups and possible breaks. Printing then uses those lengths and the remaining line width to choose breaks and indentation. The implementation in PrettyPrint.swift contains the state needed to follow nested groups and continuation lines.

This architecture is a better explanation of line-length formatting than “if the line is over 100 characters, add a newline.” A line can contain several legal break positions. Choosing one changes the space available for everything that follows, so the printer needs context about the surrounding group.

There is another subtle point in TokenStreamCreator.swift: comments are not decoration to be added later. They are read from the syntax tokens’ trivia and inserted into the printing stream. That helps explain why preserving comments is part of the design rather than an optional cleanup pass.

Linting shares the layout logic but not the rewrite

A formatter and a linter can agree about style without running exactly the same pipeline.

SwiftLinter parses the source, creates its own Context, and walks the original syntax with LintPipeline. Rule visitors emit findings for constructs that violate the configured rules. The linter does not apply FormatPipeline to produce a replacement tree.

Whitespace is handled differently. The linter calls PrettyPrinter in whitespace-only mode to obtain the expected layout, then passes that and the original source to WhitespaceLinter. That component can report layout differences while leaving the user’s file unchanged.

So two kinds of diagnostics meet in one lint operation: findings from syntax rules, and findings about whitespace. This is why “lint is just format without saving” misses part of the implementation. It is a distinct path designed for reporting.

There is an interesting trade-off here. FormatPipeline.swift explains that formatting rules currently need separate passes over the complete syntax tree: rewriting a node can affect how nearby tokens are attached, so the passes cannot simply be interleaved at individual nodes. Lint rules, by contrast, can be delegated during a visitor traversal. The implementation chooses a different traversal strategy for each job because the jobs have different constraints.

Configuration changes the result, not just the appearance

A team can start with the defaults:

swift format dump-configuration > .swift-format

The output is JSON. A smaller example of documented settings looks like this:

{
  "version": 1,
  "lineLength": 100,
  "indentation": { "spaces": 2 },
  "respectsExistingLineBreaks": true
}

The command-line tool looks for .swift-format next to the source file and then in parent directories. You can override that search with --configuration. This is especially relevant in repositories with several packages: two files can be formatted under different rules if they find different configuration files.

respectsExistingLineBreaks is a particularly revealing option. When enabled, the printer generally keeps existing line breaks that still satisfy the chosen style. When disabled, it can remove more of those breaks and produce a more uniform layout. Two developers may agree on indentation and line length yet still produce different diffs if they use different settings here.

The rules section also controls which syntax rules run. OrderedImports, for example, can reorder imports and optionally group different kinds of imports. Its implementation goes out of its way to keep comments associated with the relevant import. That is another example where a style policy requires syntax-aware rewriting, not just whitespace adjustment.

None of these defaults is an official style law for all Swift projects. The repository explicitly says its default style is one possibility. The practical goal is not to discover a universally correct brace position; it is to make a team’s chosen decisions repeatable.

Where this approach pays off, and where it doesn’t

For an app or package maintained by several people, consistent formatting can remove a noisy part of code review. The same configuration can run locally and in CI. Code generators can avoid manually placing all trivia in their emitted source. Editor tooling can use the library’s source- or syntax-based API instead of invoking a separate script for every formatting operation.

The costs are real too. Running a formatter across an existing codebase can create a large diff that hides behavioral changes, so an initial formatting pass is easier to review as its own commit. A configuration change is also a repository-wide decision: it can change the output of many files even when their logic has not changed. And if the project already uses another formatter consistently, replacing it may offer little benefit.

There is a technical limit as well. swift-format understands syntax, not the entire meaning of a program. It is not a semantic refactoring tool or a substitute for compiling and testing. A source generator using its lower-level syntax-tree API still needs to produce the right tree and satisfy the documented operator-folding assumptions.

In other words, syntax awareness makes formatting safer and more capable than a text-only approach. It does not remove the need to validate the resulting software.

A useful path through the source

If you want to study this design, start with Sources/SwiftFormat/API/SwiftFormatter.swift and SwiftLinter.swift in the same directory. They show the two public workflows without making you read the entire codebase first.

Next, follow Core/Parsing.swift, Core/FormatPipeline.swift, and Core/LintPipeline.swift. Then read Rules/DoNotUseSemicolons.swift alongside Tests/SwiftFormatTests/Rules/DoNotUseSemicolonsTests.swift: the test cases with inline comments are especially useful. Finish with PrettyPrint/TokenStreamCreator.swift, PrettyPrint/PrettyPrint.swift, and the pretty-printer design document.

The repository is licensed under Apache License 2.0 with the Swift Runtime Library Exception. Its Package.swift defines the executable, library, command plugins, and test targets; it uses SwiftSyntax, Swift Markdown, and Swift Argument Parser. The manifest explicitly lists macOS 13 and iOS 16 for package platforms, which should not be confused with a complete list of every environment where the command-line tool can be built. As of October 10, 2026, the latest GitHub release shown is 604.0.0, published September 16, 2026. The public tests and release history provide useful evidence of ongoing work, though they cannot guarantee a particular integration will be trouble-free.

The part I find most useful is not a single formatting rule. It is the decision to represent three different problems separately: what the code means syntactically, which syntax changes a style rule permits, and how that syntax should occupy lines on a screen. Once those decisions have separate places in the codebase, the formatter can handle cases such as semicolons, comments, and nested groups without turning every new style rule into another fragile text replacement.

Sources