Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

Win a ticket for the Do iOS Conference in Amsterdam Join here.

SwiftSyntax: Parse and Generate Swift source code

SwiftSyntax is a collection of Swift libraries that allow you to parse, inspect, generate, and adjust Swift source code. It was initially developed by Apple and is currently maintained as an open-source library with many contributors. You can find documentation on swiftpackageindex.com and many articles in the GitHub readme. The SwiftSyntax library is the foundation upon which tools like the Swift parser, swift-format, and Swift macros are built.

You might have used packages like SwiftDiagnostics and SwiftSyntaxMacros when working with Macros. If you’re new to Macros, I encourage you to read Swift Macros: Extend Swift with New Kinds of Expressions, as this article will be highly valuable in writing and understanding custom macros.

SwiftSyntax Releases

SwiftSyntax releases align with Swift and Xcode releases using a matching tag name. For example, the 508.0.0 tag release aligns with Swift 5.8, which comes with Xcode 14.3.

You can add SwiftSyntax as a dependency via Swift Package Manager:

// swift-tools-version:5.9
import PackageDescription

let package = Package(
    name: "MyTool",
    dependencies: [
        .package(url: "https://github.com/apple/swift-syntax.git", exact: "<#Specify Release tag#>"),
    ],
    targets: [
        .target(name: "MyTool", dependencies: [
            .product(name: "SwiftSyntax", package: "swift-syntax"),
        ]),
    ]
)

The package dependency definition should link to the specific tag release.

Exploring the Abstract Syntax Tree (AST) of Swift source code

SwiftSyntax creates an Abstract Syntax Tree (AST) of Swift source code, which allows you to interact with a high-level, safe, and efficient API. A great way to familiarize yourself with such a tree is by using swift-ast-explorer.com, developed by @kishikawakatsumi.

You can write your Swift code on the left and explore the syntax tree on the right side. As an example, I’ve written an enum with two cases:

An example of Abstract Syntax Tree (AST) exploration.
An example of Abstract Syntax Tree (AST) exploration.

If you hover over an item, you’ll get extra details like child items and error tokens.

Stay updated with the latest in Swift

Join 19,972 Swift developers in our exclusive newsletter for the latest insights, tips, and updates. Don't miss out – join today!

You can always unsubscribe, no hard feelings.

Using a Syntax Tree when writing Macros

Especially when you’re writing with attached macros, you’ll get a declaration as input via the static method. For example, when you’re writing a member macro:

public static func expansion<Declaration, Context>(
    of node: AttributeSyntax,
    providingMembersOf declaration: Declaration, /// Contains the syntax declaration.
    in context: Context
) throws -> [DeclSyntax] where Declaration : DeclGroupSyntax, Context : MacroExpansionContext

The Swift AST Explorer showed previously that our enum returns an EnumDeclSyntax item. We could use that as a guard statement to ensure our macro only allows enum attachments:

guard declaration.is(EnumDeclSyntax.self) else {
    throw CustomError.message("Only works with enums")
}

The compiler will show the thrown error in case we attach our macro to a struct:

The error gets thrown by the compiler due to attaching the macro to a struct instead of an enum.
The error gets thrown by the compiler due to attaching the macro to a struct instead of an enum.

Generating Swift Source Code

While SwiftSyntax works great for inspecting Swift code, you will also want to generate Swift code when working with features like Macros. In this example, I’m building an @EnumIdentifiable Macro inspired by David Steppenbeck. We want to create a macro that will add conformance to the Identifiable protocol for enums without associated values.

In this case, we want to attach the following code as a member:

var id: Self { self }

I recommend pasting the code you want to return into the AST Explorer to use as a source for writing the syntax to return. The above example translates to the following syntax tree written in Swift:

DeclSyntax(
    VariableDeclSyntax(bindingSpecifier: .keyword(.var), bindings: PatternBindingListSyntax(
        [
            PatternBindingSyntax(
                pattern: IdentifierPatternSyntax(identifier: .identifier("id")),
                typeAnnotation: TypeAnnotationSyntax(colon: .colonToken(), type: IdentifierTypeSyntax(name: .keyword(.`Self`))),
                accessorBlock: AccessorBlockSyntax(
                    accessors: AccessorBlockSyntax.Accessors(
                        AccessorDeclListSyntax(
                            [
                                AccessorDeclSyntax(
                                    accessorSpecifier: .keyword(.get),
                                    body: CodeBlockSyntax(
                                        leftBrace: .leftBraceToken(leadingTrivia: .space),
                                        statements: CodeBlockItemListSyntax(
                                            [
                                                CodeBlockItemSyntax(item: .stmt(
                                                    StmtSyntax(
                                                        ReturnStmtSyntax(
                                                            returnKeyword: .keyword(.return),
                                                            expression: DeclReferenceExprSyntax(baseName: .keyword(.`self`))
                                                        )
                                                    )
                                                ))
                                            ]
                                        ),
                                        rightBrace: .rightBraceToken(leadingTrivia: .newline)
                                    ))
                            ]
                        )
                    )
                )
            )
        ]
    ))
)

As you can see, this is quite complex and a lot of code to write manually. Instead, I recommend making use of trailing closures and result builders. The above would instead translate to the following code:

let variable = try VariableDeclSyntax("var id: Self") {
    StmtSyntax("return self")
}
let declaration = DeclSyntax(variable)

The big difference is writing more code as static strings, like var id: Self. The code becomes easier to understand and maintain.

Learn by tutorials

While I’m only giving a brief overview of what SwiftSyntax can offer, you might want to dive deeper into its packages. Luckily, you can find many articles and tutorials directly in the repository. After cloning the repository, you need to run Product → Build Documentation, after which the documentation will show up:

SwiftSyntax comes with rich documentation, articles, and tutorials.
SwiftSyntax comes with rich documentation, articles, and tutorials.

The documentation uses DocC and can be viewed directly as a markdown document. Not all packages are documented, but there’s much to discover.

Conclusion

SwiftSyntax allows you to parse and generate Swift source code, especially useful when writing custom Macros. Using the AST explorer, you’ll be able to learn how source code translates into an Abstract Syntax Tree. Instead of writing complex combinations of syntax nodes, it’s recommended to use trailing closures and result builders.

If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!

 
Antoine van der Lee

Written by

Antoine van der Lee

iOS Developer since 2010, former Staff iOS Engineer at WeTransfer and currently full-time Indie Developer & Founder at SwiftLee. Writing a new blog post every week related to Swift, iOS and Xcode. Regular speaker and workshop host.