The developer experience of SwiftSyntaxBuilder for Swift macros is very poor

lately, i’ve been frustrated with what i find to be an exceptionally poor user experience with implementing Swift macros that, well, generate Swift code using SwiftSyntaxBuilder.

i think, like many people, i started out using raw string interpolations, but it’s really difficult and awkward to get the formatting and indentation right when using those. so i looked for a better way, and was advised (by an LLM) to start using SwiftSyntaxBuilder instead.

for example, one might do this to build a switch-case block for a generated enum:

let decoder: SwitchExprSyntax = try .init("switch code") {
    for `case`: Case in cases {
        SwitchCaseSyntax.init("case \"\(raw: `case`.discriminant)\":") {
            "return .\(`case`.name)"
        }
    }
    SwitchCaseSyntax.init("default:") {
        "return nil"
    }
}

which is great! that is an satisfactory API that does what you need it to do. the problem is it’s absurdly difficult to generalize that knowledge to build other things, for example, EnumCaseElementListSyntax. one might start out by typing something like this:

let casesList: EnumCaseElementListSyntax = try .init {
    for `case`: Case in cases {
        EnumCaseElementSyntax.init("case \(`case`.name) = \"\(`case`.discriminant)\"")
    }
}

but this doesn’t compile and the diagnostics from the compiler are patently unhelpful.

Type '() -> ()' cannot conform to 'SyntaxProtocol'
StringUnionMacro.swift(103, 57): Only concrete types such as structs, enums and classes can conform to protocols
SyntaxProtocol.swift(49, 10): Required by initializer 'init(_:)' where 'S' = '() -> ()'
Value of optional type 'EnumCaseElementListSyntax?' must be unwrapped to a value of type 'EnumCaseElementListSyntax'
StringUnionMacro.swift(103, 57): Coalesce using '??' to provide a default when the optional value contains 'nil'
StringUnionMacro.swift(103, 57): Force-unwrap using '!' to abort execution if the optional value contains 'nil'

there’s actually no good way to deduce how to coax SwiftSyntaxBuilder to produce a EnumCaseElementListSyntax. the EnumCaseElementListSyntax type, you see, is part of the SwiftSyntax module, so its documentation doesn’t tell you anything about what SwiftSyntaxBuilder API is associated with it. it provides no examples for how to generate that type, and no links to where you can find that API.

nor does the documentation of SwiftSyntaxBuilder itself help much here, that module is completely undocumented.

as it turns out, the right invocation is this:

let casesList: EnumCaseElementListSyntax = .init {
    for `case`: Case in cases {
        EnumCaseElementSyntax.init(name: `case`.name, trailingComma: .commaToken())
    }
}

but i did not figure that out by browsing official, or even unofficial resources.

i think what is needed is not new API, but just comprehensive documentation for how to generate various SwiftSyntax nodes using SwiftSyntaxBuilder. every node in SwiftSyntax ought to come with an example snippet for how to construct that node using SwiftSyntaxBuilder. but right now, SwiftSyntax’s documentation basically acts as if SwiftSyntaxBuilder doesn’t exist at all. this is really not good.

12 Likes