前から手を出したいと思い早一年、ようやく重い腰を上げて最近 Swift macros に入門しました。
なかなかはじめられなかった理由は、私が面倒くさがりというのも大きいですが、Swift macros の実装が通常の iOS 開発などでは見慣れない API が飛び交い、勝手がかなり違いそうというイメージで入門のハードルが高いように感じていました。
ただ、実際に手を出してみると想像していたほど訳のわからないものでもなく、比較的取り組みやすい環境が Apple から提供されていると感じました。
この記事では、Swift macros に入門した私の経験談から、Swift macros 意外と怖くないよというのを伝えたいという趣向の記事です。
対象読者
- Swiftの基本的な文法がわかる
- Swift macros知っているけど、まだ自分で実装したことはない
環境
参考までに、私が Swift macros を実装した時の環境は以下の通りです
- Xcode 16.4
- Swift 6.1
- macOS 15.4.1
Swift macros の実装準備
Swift macros は SPM の Package から実装できます。
以下のように、Xcode の File->New->Package を選択し、Package のテンプレートリストを表示させる。

テンプレートの中に Swift Macro があるので、これを選択すると Swift macros の実装用の Package が生成されます。

以下のような構成の Package が生成されます。(ちょっと長いので、折りたたんでいます)
生成されるPackage.swiftの内容
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
import CompilerPluginSupport
let package = Package(
name: "MacroExample",
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "MacroExample",
targets: ["MacroExample"]
),
.executable(
name: "MacroExampleClient",
targets: ["MacroExampleClient"]
),
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
// Macro implementation that performs the source transformation of a macro.
.macro(
name: "MacroExampleMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
// Library that exposes a macro as part of its API, which is used in client programs.
.target(name: "MacroExample", dependencies: ["MacroExampleMacros"]),
// A client of the library, which is able to use the macro in its own code.
.executableTarget(name: "MacroExampleClient", dependencies: ["MacroExample"]),
// A test target used to develop the macro implementation.
.testTarget(
name: "MacroExampleTests",
dependencies: [
"MacroExampleMacros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
]
)
これで、Swift macros を実装する準備は完了です。
サンプルの題材
今回サンプルとして、classやstructに対してマクロを付与するだけで、ModelConvertibleという protocol に準拠されるというもの題材として、Swift macros意外と簡単やでというのを頑張って伝えていきます。
サンプルのマクロの内容の詳細は以下の通りです。
以下がマクロを付与した時に準拠できるprotocolです。
protocol ModelConvertible {
associatedtype ModelType: Sendable
init(_ model: ModelType) throws
func toModel() throws -> ModelType
}
仮に以下のようなSampleDataという型があったとします。
struct SampleData {
let id: String
let text: String
}
SampleDataにModelConvertibleを準拠させると以下のような記述が必要になります。
SampleDataにマクロを付与した時、extensionの定義をまるまる自動生成されるようなイメージです。
extension SampleData: ModelConvertible {
typealias ModelType = SampleModel
init(_ model: SampleModel) throws {
id = model.id
text = model.text
}
func toModel() throws -> SampleModel {
return .init(id: id, text: text)
}
}
struct SampleModel: Sendable {
let id: String
let text: String
}
まずは何のマクロで実装するかを考える
Swift macros にはいくつか種類があり、種類によってできることとできないことがあります。
大きく分けると Freestanding Macro と Attached Macro があります。
Freestanding Macro は、既存の Swift コードは参照せず、受け取ったパラメータのみを使って自立的にコードを生成するマクロです。
Attached Macro はその逆で、既存の Swift コードを参照して、付与した Swift コード毎に生成するコードを変えるマクロです。
この2種類の中にも複数の種類のマクロが存在します。
マクロの種類を詳しく説明しだすと本記事の本質とずれてしまうので、あえて割愛しますがすでにわかりやすく解説されている素晴らしい記事があるので、興味がある方はこちら参照いただければと思います。
今回の題材では、既存の型に対してマクロを付与して、extensionを生成したいので、Attached Macro の ExtensionMacro を使って実装していきます。
Swift macros の構成要素
Swift macros の構成要素として、インターフェースと実装部分に分かれています。
先で作成した Swift macros 実装用の Package では以下のような依存関係になっています。
MacroExampleClient(Swift macrosを使用するやつ)
↓
MacroExample(インターフェース)
↓
MacroExampleMacros(実装部分)
Swift macros を実際に使用するモジュールは Swift macros が実装されているモジュールを直接知っているわけではなく、インターフェース経由で Swift macros を使用するようになっています。
そのため、Swift macros を実装するには最低限、以下を実装する必要があります。
- Swift macros のインターフェース
- Swift macros の実装部分
Swift macros の実装
インターフェース部分の実装
まずはインターフェースを実装していきますが、これは Swift macros の Package 生成でデフォルトで実装されているstringifyマクロを見ながらやるのがわかりやすいです。
MacroExampleモジュールのMacroExample.swiftを見てみると以下が定義されています。
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MacroExampleMacros", type: "StringifyMacro")
何だか見慣れない文法があって戸惑うかもしれませんが意外とシンプルなので、大丈夫です!
まずよくわからないポイントは@freestanding(expression)のところでしょう。
これは先にチラッと説明したマクロの種類を宣言する部分です。
これをみると,Freestanding Macro の ExpressionMacroであると宣言しています。(ExpressionMacroは評価式を生成するマクロで普通の関数の挙動とそう変わらないイメージで動作するマクロですが、詳細が気になる方は各自調べてみてください。)
今回の題材であれば Attached Macro のExtensionMacroを実装したいので、@attached(extension)となります。(実はこのあともう少し記述を足しますが、後ほど説明を入れます)
続いて 2 行目の以下部分については、関数シグネチャみたいなものです。
public macro stringify<T>(_ value: T) -> (T, String)
関数定義のfuncキーワードがmacroになるだけで他は関数定義と意味合いは変わりません。
呼び出す時の名前、引数、戻り値、Generics など関数と同じふうにインターフェースが定義できます。
最後に、2 行目の以下部分でインターフェースとマクロの実装部分を対応付けています。
= #externalMacro(module: "MacroExampleMacros", type: "StringifyMacro")
moduleにはマクロの実装が定義されているモジュール名を指定し、
typeには実装したマクロの型名を指定することで、実際の挙動を設定できる。
上記を踏まえて、今回実装するマクロのインターフェースは以下のようになります。
@attached(
extension,
conformances: ModelConvertible,
names: named(init), named(ModelType), named(toModel)
)
public macro modelConvert<T: Sendable>(_ type: T.Type) = #externalMacro(module: "MacroExampleMacros", type: "ModelConvertMacro")
ModelConvertibleに適合するextensionを生成するにあたって、ModelConvertible.ModelTypeに指定する型を知るためにパラメーターとして指定できるようにしています。
現状 Attached Macro では付与した定義の外にある定義は読み取ることができないので、付与する定義外の情報を取得しようとするとパラメーターから受け付ける必要があります。
また、生成したextensionに protocol を適合させる場合は、@attachedにconformancesで適合させる protocol の型を指定する必要があります。
そのため、conformances: ModelConvertibleというのを追加しています。
conformancesに型を指定する場合は、ModelConvertibleの型情報が必要になるため、MacroExampleモジュール側内でModelConvertibleを定義して参照できるようにしておきます。
マクロによって新たに宣言を追加する場合は、namesで明示的に追加される宣言の名前を指定する必要があります。
namesには、named(<宣言名>)で指定できます。
今回は追加される宣言の名前が決まっており静的なので、namedで追加する宣言名を指定していますが、付与する対象によって生成する宣言の名前が動的に変わる場合はarbitraryを指定することで対応できます。
マクロの挙動部分を実装
マクロの挙動部分の最小実装
まずは、前項で実装したインターフェースの#externalMacroのmoduleに指定したモジュールに、typeで指定した型名の構造体を定義します。
その型をExtensionMacroという protocol に適合させることで、extensionを生成するマクロが実装できます。
先述したマクロの種類によって、適合させる protocol も変わります。
ExtensionMacroに適合させるには、expansionメソッドの実装が必要になります。
public struct ModelConvertMacro: ExtensionMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
return []
}
}
expansionメソッドの戻り値としてextensionブロックの定義を表すExtensionDeclSyntaxの配列を返します。
ここで返したextensionブロックの定義がそのまま自動生成されます。
今は空の配列を返しているので、何も自動生成しませんが、一旦こちらがマクロの最小実装です。
マクロのテスト実装
マクロの挙動部分の実装に取り掛かる前に、実装したいマクロのテストを先に実装しておくことをお勧めします。
TDD のように先にテストで期待値を定義してテストを実行しながら実装することで、素早く正確に実装できます。
テストがあるとブレイクポイント張って実行結果を細かく確認することもできますし、後述しますが、lldbコマンドを使用することでSwiftSyntaxの理解の助けにもなります。
TDD を用いてマクロを実装する方法は、WWDC のビデオの「Swift マクロの書き方」でも言及されています。
https://developer.apple.com/jp/videos/play/wwdc2023/10166/
Package を生成したときにデフォルトでMacroExampleTestsというテストターゲットが生成されていると思うので、その中に以下のようなテストケースを実装します。
func testModelConvert() throws {
assertMacroExpansion(
"""
@modelConvert(SampleModel.self)
struct SampleData {
let id: String
let text: String
}
""",
expandedSource: """
struct SampleData {
let id: String
let text: String
}
extension SampleData: ModelConvertible {
typealias ModelType = SampleModel
init(_ model: SampleModel) throws {
id = model.id
text = model.text
}
func toModel() throws -> SampleModel {
return .init(id: id, text: text)
}
}
""",
macros: ["modelConvert": ModelConvertMacro.self]
)
}
最初は自分もちょっと驚いたのですが、マクロのテストはマクロ適用後のコードを文字列として比較します。
アサーションとして使うのはassertMacroExpansionメソッドで以下引数を設定しています。
- 第一引数: 入力値、マクロを付与されるコードとマクロを文字列として渡す
- expandedSource: 期待値、マクロを付与されたコードとマクロ展開後のコードを文字列として渡す。
- macros: マクロ名とマクロの型の情報を Dictionary 型として設定する。
このテストを実行することで、実装した内容のフィードバックを高速で得ることができるので、正確な実装を素早く行いやすいです。
また、SwiftSyntax の API は最初は実行結果が予測しずらく難しく感じますが、ブレイクポイントを張って処理を止めて、lldb コマンドでAPIの実行結果を確認することで難しさがかなり解消されます。
百聞は一見に如かずなので、実際にを試してみていただければと思います。
マクロの実装の流れをざっくり確認
まだマクロがどのように動作するかイメージしずらいと思うので、簡単な挙動にしてどのような流れで実装してマクロが出来上がるのかざっくり確認していきます。
先ほど実装したModelConvertMacroのexpansionメソッドの戻り値を以下に書き換えてみましょう。
return [.init(extendedType: type, memberBlock: .init(members: []))]
MacroExampleClientモジュールのmain.swiftで、以下の実装を追加します。
@modelConvert(SampleModel.self)
struct Hoge {}
@modelConvertの部分を右クリックしてExpand Macroを選択すると以下のようにマクロで生成されたコードを確認できるので、確認してみると空のextensionが生成されていることが確認できます。
戻り値にExtensionDeclSyntaxを設定すると、ちゃんとextensionが自動生成されていることがわかりました。
続いてマクロの引数やマクロを付与した定義を読み取って、extensionの中身を生成する部分の実装の流れについてです。
ここが一番難所ですが、それと同時に思っているほど難しくもないところなので、頑張って意外と簡単だよと言うことを伝えるように頑張ります。
extensionを生成するために、生成したい定義をExtensionDeclSyntaxとして実装する必要があることは先ほどの説明の通りです。
ExtensionDeclSyntaxといった SwiftSyntax の型はSyntaxProtocolに適合しており、lldbコマンドなどでprintするとツリー構造として表現できることがわかります。
ツリーの内容を見ることで、どのようにExtensionDeclSyntaxを生成していけば良いかがわかるようになります。
実際に完成版のExtensionDeclSyntaxを print すると以下のようなアスキーアートで表示されます。(長いので折りたたんでいます)
実装したいExtensionDeclSyntaxのツリー
ExtensionDeclSyntax
├─attributes: AttributeListSyntax
├─modifiers: DeclModifierListSyntax
├─extensionKeyword: keyword(SwiftSyntax.Keyword.extension)
├─extendedType: IdentifierTypeSyntax
│ ╰─name: identifier("SampleData")
├─inheritanceClause: InheritanceClauseSyntax
│ ├─colon: colon
│ ╰─inheritedTypes: InheritedTypeListSyntax
│ ╰─[0]: InheritedTypeSyntax
│ ╰─type: IdentifierTypeSyntax
│ ╰─name: identifier("ModelConvertible")
╰─memberBlock: MemberBlockSyntax
├─leftBrace: leftBrace
├─members: MemberBlockItemListSyntax
│ ├─[0]: MemberBlockItemSyntax
│ │ ╰─decl: TypeAliasDeclSyntax
│ │ ├─attributes: AttributeListSyntax
│ │ ├─modifiers: DeclModifierListSyntax
│ │ ├─typealiasKeyword: keyword(SwiftSyntax.Keyword.typealias)
│ │ ├─name: identifier("ModelType")
│ │ ╰─initializer: TypeInitializerClauseSyntax
│ │ ├─equal: equal
│ │ ╰─value: IdentifierTypeSyntax
│ │ ╰─name: identifier("SampleModel")
│ ├─[1]: MemberBlockItemSyntax
│ │ ╰─decl: InitializerDeclSyntax
│ │ ├─attributes: AttributeListSyntax
│ │ ├─modifiers: DeclModifierListSyntax
│ │ ├─initKeyword: keyword(SwiftSyntax.Keyword.init)
│ │ ├─signature: FunctionSignatureSyntax
│ │ │ ├─parameterClause: FunctionParameterClauseSyntax
│ │ │ │ ├─leftParen: leftParen
│ │ │ │ ├─parameters: FunctionParameterListSyntax
│ │ │ │ │ ╰─[0]: FunctionParameterSyntax
│ │ │ │ │ ├─attributes: AttributeListSyntax
│ │ │ │ │ ├─modifiers: DeclModifierListSyntax
│ │ │ │ │ ├─firstName: wildcard
│ │ │ │ │ ├─secondName: identifier("model")
│ │ │ │ │ ├─colon: colon
│ │ │ │ │ ╰─type: IdentifierTypeSyntax
│ │ │ │ │ ╰─name: identifier("SampleModel")
│ │ │ │ ╰─rightParen: rightParen
│ │ │ ╰─effectSpecifiers: FunctionEffectSpecifiersSyntax
│ │ │ ╰─throwsClause: ThrowsClauseSyntax
│ │ │ ╰─throwsSpecifier: keyword(SwiftSyntax.Keyword.throws)
│ │ ╰─body: CodeBlockSyntax
│ │ ├─leftBrace: leftBrace
│ │ ├─statements: CodeBlockItemListSyntax
│ │ │ ├─[0]: CodeBlockItemSyntax
│ │ │ │ ╰─item: SequenceExprSyntax
│ │ │ │ ╰─elements: ExprListSyntax
│ │ │ │ ├─[0]: DeclReferenceExprSyntax
│ │ │ │ │ ╰─baseName: identifier("id")
│ │ │ │ ├─[1]: AssignmentExprSyntax
│ │ │ │ │ ╰─equal: equal
│ │ │ │ ╰─[2]: MemberAccessExprSyntax
│ │ │ │ ├─base: DeclReferenceExprSyntax
│ │ │ │ │ ╰─baseName: identifier("model")
│ │ │ │ ├─period: period
│ │ │ │ ╰─declName: DeclReferenceExprSyntax
│ │ │ │ ╰─baseName: identifier("id")
│ │ │ ╰─[1]: CodeBlockItemSyntax
│ │ │ ╰─item: SequenceExprSyntax
│ │ │ ╰─elements: ExprListSyntax
│ │ │ ├─[0]: DeclReferenceExprSyntax
│ │ │ │ ╰─baseName: identifier("text")
│ │ │ ├─[1]: AssignmentExprSyntax
│ │ │ │ ╰─equal: equal
│ │ │ ╰─[2]: MemberAccessExprSyntax
│ │ │ ├─base: DeclReferenceExprSyntax
│ │ │ │ ╰─baseName: identifier("model")
│ │ │ ├─period: period
│ │ │ ╰─declName: DeclReferenceExprSyntax
│ │ │ ╰─baseName: identifier("text")
│ │ ╰─rightBrace: rightBrace
│ ╰─[2]: MemberBlockItemSyntax
│ ╰─decl: FunctionDeclSyntax
│ ├─attributes: AttributeListSyntax
│ ├─modifiers: DeclModifierListSyntax
│ ├─funcKeyword: keyword(SwiftSyntax.Keyword.func)
│ ├─name: identifier("toModel")
│ ├─signature: FunctionSignatureSyntax
│ │ ├─parameterClause: FunctionParameterClauseSyntax
│ │ │ ├─leftParen: leftParen
│ │ │ ├─parameters: FunctionParameterListSyntax
│ │ │ ╰─rightParen: rightParen
│ │ ├─effectSpecifiers: FunctionEffectSpecifiersSyntax
│ │ │ ╰─throwsClause: ThrowsClauseSyntax
│ │ │ ╰─throwsSpecifier: keyword(SwiftSyntax.Keyword.throws)
│ │ ╰─returnClause: ReturnClauseSyntax
│ │ ├─arrow: arrow
│ │ ╰─type: IdentifierTypeSyntax
│ │ ╰─name: identifier("SampleModel")
│ ╰─body: CodeBlockSyntax
│ ├─leftBrace: leftBrace
│ ├─statements: CodeBlockItemListSyntax
│ │ ╰─[0]: CodeBlockItemSyntax
│ │ ╰─item: ReturnStmtSyntax
│ │ ├─returnKeyword: keyword(SwiftSyntax.Keyword.return)
│ │ ╰─expression: FunctionCallExprSyntax
│ │ ├─calledExpression: MemberAccessExprSyntax
│ │ │ ├─period: period
│ │ │ ╰─declName: DeclReferenceExprSyntax
│ │ │ ╰─baseName: keyword(SwiftSyntax.Keyword.init)
│ │ ├─leftParen: leftParen
│ │ ├─arguments: LabeledExprListSyntax
│ │ │ ├─[0]: LabeledExprSyntax
│ │ │ │ ├─label: identifier("id")
│ │ │ │ ├─colon: colon
│ │ │ │ ├─expression: DeclReferenceExprSyntax
│ │ │ │ │ ╰─baseName: identifier("id")
│ │ │ │ ╰─trailingComma: comma
│ │ │ ╰─[1]: LabeledExprSyntax
│ │ │ ├─label: identifier("text")
│ │ │ ├─colon: colon
│ │ │ ╰─expression: DeclReferenceExprSyntax
│ │ │ ╰─baseName: identifier("text")
│ │ ├─rightParen: rightParen
│ │ ╰─additionalTrailingClosures: MultipleTrailingClosureElementListSyntax
│ ╰─rightBrace: rightBrace
╰─rightBrace: rightBrace
このツリーの内容を見ると、SyntaxProtocolに適合する型とそのプロパティでツリー構造を表しており、これはSyntaxProtocolに適合する型のイニシャライザにも対応しています。
例えば、以下ツリーと同じExtensionDeclSyntaxを定義しようとすると以下のようなイニシャライザでExtensionDeclSyntaxを生成できます。
ExtensionDeclSyntax
├─attributes: AttributeListSyntax
├─modifiers: DeclModifierListSyntax
├─extensionKeyword: keyword(SwiftSyntax.Keyword.extension)
├─extendedType: IdentifierTypeSyntax
│ ╰─name: identifier("SampleData")
├─inheritanceClause: InheritanceClauseSyntax
│ ├─colon: colon
│ ╰─inheritedTypes: InheritedTypeListSyntax
│ ╰─[0]: InheritedTypeSyntax
│ ╰─type: IdentifierTypeSyntax
│ ╰─name: identifier("ModelConvertible")
╰─memberBlock: MemberBlockSyntax
├─leftBrace: leftBrace
├─...
...
ExtensionDeclSyntax(
extensionKeyword: <#T##TokenSyntax#>
extendedType: <#T##TypeSyntaxProtocol#>,
inheritanceClause: <#T##InheritanceClauseSyntax?#>
memberBlock: <#T##MemberBlockSyntax#>
)
attributesやmodifiersにはツリーの内容を見ると特に何も設定されていないので、省略しています。
イニシャライザのパラメータにはデフォルト値が入っていたりで省略することができるものもあります。
このように実装したいコードのツリー構造がわかれば、SyntaxProtocolの型のイニシャライザで大体実装可能です。
そして、SyntaxProtocolの型はそのままコードを表しているので、ツリー構造がわかればそれをもとにマクロで生成したいコードも作れます。
自動生成したいコードのツリー内容の確認方法
自動生成したいコードを確認するために、ModelConvertMacroが定義されているファイルにimport SwiftParserを追加して以下コードをexpansionメソッドに仕込んでおきます。
var parser = Parser("""
extension SampleData: ModelConvertible {
typealias ModelType = SampleModel
init(_ model: SampleModel) throws {
id = model.id
text = model.text
}
func toModel() throws -> SampleModel {
return .init(id: id, text: text)
}
}
""")
let block = CodeBlockSyntax.parse(from: &parser)
上記のようにParserを使うことで文字列のコードから SwiftSyntax の型に変換することができます。
Parserのイニシャライザに今回自動生成したいコードを文字列として指定することで、SwiftSyntax の型に変換後その内容を print で確認することで生成したいコードのツリー内容を確認できます。
expansionメソッドの return の行にブレイクポイントを張って、先ほど実装したテストを実行してみます。
上記のように return 時点で止まってくれるので、expansionメソッド内で定義した変数blockを lldb コマンドの po で print すると、Parserに設定したコードのツリー内容を確認できます。
(lldb) po block
以下が表示されますが、Parserから SwiftSyntax の型に変換するときにCodeBlockSyntaxに変換することになるので実際に知りたいextensionの部分は
CodeBlockSyntax->statements->[0]->item
の部分からになります。(長いので折りたたんでいます)
変数blockのツリー内容
CodeBlockSyntax
├─leftBrace: leftBrace MISSING
├─statements: CodeBlockItemListSyntax
│ ╰─[0]: CodeBlockItemSyntax
│ ╰─item: ExtensionDeclSyntax
│ ├─attributes: AttributeListSyntax
│ ├─modifiers: DeclModifierListSyntax
│ ├─extensionKeyword: keyword(SwiftSyntax.Keyword.extension)
│ ├─extendedType: IdentifierTypeSyntax
│ │ ╰─name: identifier("SampleData")
│ ├─inheritanceClause: InheritanceClauseSyntax
│ │ ├─colon: colon
│ │ ╰─inheritedTypes: InheritedTypeListSyntax
│ │ ╰─[0]: InheritedTypeSyntax
│ │ ╰─type: IdentifierTypeSyntax
│ │ ╰─name: identifier("ModelConvertible")
│ ╰─memberBlock: MemberBlockSyntax
│ ├─leftBrace: leftBrace
│ ├─members: MemberBlockItemListSyntax
│ │ ├─[0]: MemberBlockItemSyntax
│ │ │ ╰─decl: TypeAliasDeclSyntax
│ │ │ ├─attributes: AttributeListSyntax
│ │ │ ├─modifiers: DeclModifierListSyntax
│ │ │ ├─typealiasKeyword: keyword(SwiftSyntax.Keyword.typealias)
│ │ │ ├─name: identifier("ModelType")
│ │ │ ╰─initializer: TypeInitializerClauseSyntax
│ │ │ ├─equal: equal
│ │ │ ╰─value: IdentifierTypeSyntax
│ │ │ ╰─name: identifier("SampleModel")
│ │ ├─[1]: MemberBlockItemSyntax
│ │ │ ╰─decl: InitializerDeclSyntax
│ │ │ ├─attributes: AttributeListSyntax
│ │ │ ├─modifiers: DeclModifierListSyntax
│ │ │ ├─initKeyword: keyword(SwiftSyntax.Keyword.init)
│ │ │ ├─signature: FunctionSignatureSyntax
│ │ │ │ ├─parameterClause: FunctionParameterClauseSyntax
│ │ │ │ │ ├─leftParen: leftParen
│ │ │ │ │ ├─parameters: FunctionParameterListSyntax
│ │ │ │ │ │ ╰─[0]: FunctionParameterSyntax
│ │ │ │ │ │ ├─attributes: AttributeListSyntax
│ │ │ │ │ │ ├─modifiers: DeclModifierListSyntax
│ │ │ │ │ │ ├─firstName: wildcard
│ │ │ │ │ │ ├─secondName: identifier("model")
│ │ │ │ │ │ ├─colon: colon
│ │ │ │ │ │ ╰─type: IdentifierTypeSyntax
│ │ │ │ │ │ ╰─name: identifier("SampleModel")
│ │ │ │ │ ╰─rightParen: rightParen
│ │ │ │ ╰─effectSpecifiers: FunctionEffectSpecifiersSyntax
│ │ │ │ ╰─throwsClause: ThrowsClauseSyntax
│ │ │ │ ╰─throwsSpecifier: keyword(SwiftSyntax.Keyword.throws)
│ │ │ ╰─body: CodeBlockSyntax
│ │ │ ├─leftBrace: leftBrace
│ │ │ ├─statements: CodeBlockItemListSyntax
│ │ │ │ ├─[0]: CodeBlockItemSyntax
│ │ │ │ │ ╰─item: SequenceExprSyntax
│ │ │ │ │ ╰─elements: ExprListSyntax
│ │ │ │ │ ├─[0]: DeclReferenceExprSyntax
│ │ │ │ │ │ ╰─baseName: identifier("id")
│ │ │ │ │ ├─[1]: AssignmentExprSyntax
│ │ │ │ │ │ ╰─equal: equal
│ │ │ │ │ ╰─[2]: MemberAccessExprSyntax
│ │ │ │ │ ├─base: DeclReferenceExprSyntax
│ │ │ │ │ │ ╰─baseName: identifier("model")
│ │ │ │ │ ├─period: period
│ │ │ │ │ ╰─declName: DeclReferenceExprSyntax
│ │ │ │ │ ╰─baseName: identifier("id")
│ │ │ │ ╰─[1]: CodeBlockItemSyntax
│ │ │ │ ╰─item: SequenceExprSyntax
│ │ │ │ ╰─elements: ExprListSyntax
│ │ │ │ ├─[0]: DeclReferenceExprSyntax
│ │ │ │ │ ╰─baseName: identifier("text")
│ │ │ │ ├─[1]: AssignmentExprSyntax
│ │ │ │ │ ╰─equal: equal
│ │ │ │ ╰─[2]: MemberAccessExprSyntax
│ │ │ │ ├─base: DeclReferenceExprSyntax
│ │ │ │ │ ╰─baseName: identifier("model")
│ │ │ │ ├─period: period
│ │ │ │ ╰─declName: DeclReferenceExprSyntax
│ │ │ │ ╰─baseName: identifier("text")
│ │ │ ╰─rightBrace: rightBrace
│ │ ╰─[2]: MemberBlockItemSyntax
│ │ ╰─decl: FunctionDeclSyntax
│ │ ├─attributes: AttributeListSyntax
│ │ ├─modifiers: DeclModifierListSyntax
│ │ ├─funcKeyword: keyword(SwiftSyntax.Keyword.func)
│ │ ├─name: identifier("toModel")
│ │ ├─signature: FunctionSignatureSyntax
│ │ │ ├─parameterClause: FunctionParameterClauseSyntax
│ │ │ │ ├─leftParen: leftParen
│ │ │ │ ├─parameters: FunctionParameterListSyntax
│ │ │ │ ╰─rightParen: rightParen
│ │ │ ├─effectSpecifiers: FunctionEffectSpecifiersSyntax
│ │ │ │ ╰─throwsClause: ThrowsClauseSyntax
│ │ │ │ ╰─throwsSpecifier: keyword(SwiftSyntax.Keyword.throws)
│ │ │ ╰─returnClause: ReturnClauseSyntax
│ │ │ ├─arrow: arrow
│ │ │ ╰─type: IdentifierTypeSyntax
│ │ │ ╰─name: identifier("SampleModel")
│ │ ╰─body: CodeBlockSyntax
│ │ ├─leftBrace: leftBrace
│ │ ├─statements: CodeBlockItemListSyntax
│ │ │ ╰─[0]: CodeBlockItemSyntax
│ │ │ ╰─item: ReturnStmtSyntax
│ │ │ ├─returnKeyword: keyword(SwiftSyntax.Keyword.return)
│ │ │ ╰─expression: FunctionCallExprSyntax
│ │ │ ├─calledExpression: MemberAccessExprSyntax
│ │ │ │ ├─period: period
│ │ │ │ ╰─declName: DeclReferenceExprSyntax
│ │ │ │ ╰─baseName: keyword(SwiftSyntax.Keyword.init)
│ │ │ ├─leftParen: leftParen
│ │ │ ├─arguments: LabeledExprListSyntax
│ │ │ │ ├─[0]: LabeledExprSyntax
│ │ │ │ │ ├─label: identifier("id")
│ │ │ │ │ ├─colon: colon
│ │ │ │ │ ├─expression: DeclReferenceExprSyntax
│ │ │ │ │ │ ╰─baseName: identifier("id")
│ │ │ │ │ ╰─trailingComma: comma
│ │ │ │ ╰─[1]: LabeledExprSyntax
│ │ │ │ ├─label: identifier("text")
│ │ │ │ ├─colon: colon
│ │ │ │ ╰─expression: DeclReferenceExprSyntax
│ │ │ │ ╰─baseName: identifier("text")
│ │ │ ├─rightParen: rightParen
│ │ │ ╰─additionalTrailingClosures: MultipleTrailingClosureElementListSyntax
│ │ ╰─rightBrace: rightBrace
│ ╰─rightBrace: rightBrace
╰─rightBrace: rightBrace MISSING
ここまでで、以下がわかったので、どのような流れで実装していくかのイメージが何となく見えてきました。
- マクロの最小実装について
- マクロのテストについて
- SwiftSyntaxのツリー内容から同じ構造のSwiftSyntaxのインスタンスが生成できること
- テストのデバッグ機能を使って、ツリー内容が確認できること
マクロの最小実装、テスト実装ができたら、
自動生成したいコードのツリー内容を確認して、ツリーの内容をもとにSwiftSyntaxの型のオブジェクトを生成して、そのオブジェクトをexpansionメソッドの戻り値に指定すればよさそうです。
この流れで、実際にサンプルマクロの挙動部分の実装に取り掛かります。
サンプルマクロの挙動部分を実装
ここから説明してきた知識をもとにマクロの挙動を実装していきます。
まずは出力したツリー内容のうち、以下部分を実装していきます。
ExtensionDeclSyntax
├─attributes: AttributeListSyntax
├─modifiers: DeclModifierListSyntax
├─extensionKeyword: keyword(SwiftSyntax.Keyword.extension)
├─extendedType: IdentifierTypeSyntax
│ ╰─name: identifier("SampleData")
├─inheritanceClause: InheritanceClauseSyntax
│ ├─colon: colon
│ ╰─inheritedTypes: InheritedTypeListSyntax
│ ╰─[0]: InheritedTypeSyntax
│ ╰─type: IdentifierTypeSyntax
│ ╰─name: identifier("ModelConvertible")
...
実装すると以下のようになります。
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
return [.init(
extensionKeyword: .keyword(.extension),
extendedType: type,
inheritanceClause: .init(
colon: .colonToken(),
inheritedTypes: .init(itemsBuilder: {
InheritedTypeSyntax(
type: IdentifierTypeSyntax(
name: .identifier("ModelConvertible")
)
)
})
),
memberBlock: .init(members: [])
)]
}
先の例ではスルーしていましたが、extendedTypeについて、expansionメソッドのパラメーターのtypeを設定しています。
typeにはマクロを付与した宣言の型名が SwiftSyntax の型であるTypeSyntaxProtocolとして格納されています。
typeに何が入っているかは、公式のドキュメント�らもわかりますが、テスト実行時に、lldbコマンドでtypeをprintしてみることでも確認できます。
po type
expansionメソッドのパラメーターに対して全てlldbコマンドで中身を見てみると、何が渡されるかのイメージがつきやすいです。
ツリーの内容を見るとextendedTypeにはextensionの型名が入っているので、type`をそのまま渡しています。
続いてツリーのmembersの一つ目のMemberBlockItemSyntaxを実装していきます。
TypeAliasDeclSyntaxという名前からも想像つくと思いますが、typealiasの行の生成部分になります。
ExtensionDeclSyntax
├─attributes: ...
├─modifiers: ...
├─extensionKeyword: ...
├─extendedType: ...
├─...
╰─memberBlock: MemberBlockSyntax
├─leftBrace: leftBrace
├─members: MemberBlockItemListSyntax
│ ├─[0]: MemberBlockItemSyntax
│ │ ╰─decl: TypeAliasDeclSyntax
│ │ ├─attributes: AttributeListSyntax
│ │ ├─modifiers: DeclModifierListSyntax
│ │ ├─typealiasKeyword: keyword(SwiftSyntax.Keyword.typealias)
│ │ ├─name: identifier("ModelType")
│ │ ╰─initializer: TypeInitializerClauseSyntax
│ │ ├─equal: equal
│ │ ╰─value: IdentifierTypeSyntax
│ │ ╰─name: identifier("SampleModel")
...
インターフェース実装のところで軽く触れましたが、typealiasに指定する型はマクロのパラメータで指定されるようにしています。
そのためまずはマクロの引数取得の処理を実装します。
マクロの引数はexpansionメソッドのパラメーターのnodeに格納されていますが、nodeにはマクロのコードの部分が SwiftSyntax の型として格納されているに過ぎないので、ツリー内容を取得して、引数に指定されてる型の文字列を取得します。
ツリー内容の取得に関しては、ブレイクポイントを張ってからのテスト実行により、lldb コマンドで確認します。
AttributeSyntax
├─atSign: atSign
├─attributeName: IdentifierTypeSyntax
│ ╰─name: identifier("ModelConvert")
├─leftParen: leftParen
├─arguments: LabeledExprListSyntax
│ ╰─[0]: LabeledExprSyntax
│ ╰─expression: MemberAccessExprSyntax
│ ├─base: DeclReferenceExprSyntax
│ │ ╰─baseName: identifier("SampleModel")
│ ├─period: period
│ ╰─declName: DeclReferenceExprSyntax
│ ╰─baseName: keyword(SwiftSyntax.Keyword.self)
╰─rightParen: rightParen
テストコードでは、マクロのパラメーターに渡した型はSampleModelなので、上記ツリーからSampleModelという文字列を取得しにいくことを考えればよさそうです。
取得処理の実装例として以下のように書けます。
let modelTypeToken = node.arguments!.cast(LabeledExprListSyntax.self).first!
.expression.cast(MemberAccessExprSyntax.self)
.base?.cast(DeclReferenceExprSyntax.self)
.baseName
SampleModelはnodeのargumentsプロパティ配下にあるので、まずはnode.argumentsとargumentsを参照します。
ツリー内容にはargumentsはLabeledExprListSyntaxとなっているが、実際にnode.argumentsの型をみてみるとAttributeSyntax.Arguments?になっています。
ただAttributeSyntax.Arguments?はCollectionなどに適合していないのでこのままではツリー通りに配下の要素には参照できません。
そんな時は、cast(_:)やas(_:)を使って型を明示的に指定することができます。
今回はツリーの通りにLabeledExprListSyntaxに変換するため.cast(LabeledExprListSyntax.self)と繋げています。
このようにツリー内容から欲しい情報を SwiftSyntax の型から取得することができます。
要領は同じなので、以降の説明は割愛する。
あとは先ほどと同じ要領で、ツリーの内容通りにtypealiasのコードを SwiftSyntax で追加していきます。
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
let modelTypeToken = node.arguments!.cast(LabeledExprListSyntax.self).first!
.expression.cast(MemberAccessExprSyntax.self)
.base?.cast(DeclReferenceExprSyntax.self)
.baseName
return [.init(
extensionKeyword: .keyword(.extension),
extendedType: type,
inheritanceClause: .init(
colon: .colonToken(),
inheritedTypes: .init(itemsBuilder: {
InheritedTypeSyntax(
type: IdentifierTypeSyntax(
name: .identifier("ModelConvertible")
)
)
})
),
memberBlock: .init(members: .init(itemsBuilder: {
MemberBlockItemSyntax(
decl: TypeAliasDeclSyntax(
name: .identifier("ModelType"),
initializer: TypeInitializerClauseSyntax(
value: IdentifierTypeSyntax(name: modelTypeToken!)
)
)
)
}))
)]
}
これが実装できたら、以下のようにtypealiasが自動生成されていることが確認できると思う。

次にMemberBlockItemListSyntaxの二つ目の要素の部分について実装していきます。
MemberBlockItemSyntaxのdeclの型がInitializerDeclSyntaxとなっているので、イニシャライザのコードということが何となくわかります。
SwiftSyntaxの型名も最初は何のことかイメージしづらいですが、よくみるとわかりやすいものも結構あります。
InitializerDeclSyntaxもそうですし、今回生成しようとしているExtensionDeclSyntaxもextensionの宣言のことだとわかりやすい。
Swift AST Explorerを用いると、どのコードが何のSwiftSyntaxの型名になるのかさらにわかりやすくなるので、こちらで遊んでみるのもSwiftSyntaxに慣れる上でおすすめです。
ExtensionDeclSyntax
├─...
╰─memberBlock: MemberBlockSyntax
├─leftBrace: leftBrace
├─members: MemberBlockItemListSyntax
│ ├─[0]: ...
│ ├─[1]: MemberBlockItemSyntax
│ │ ╰─decl: InitializerDeclSyntax
│ │ ├─attributes: AttributeListSyntax
│ │ ├─modifiers: DeclModifierListSyntax
│ │ ├─initKeyword: keyword(SwiftSyntax.Keyword.init)
│ │ ├─signature: FunctionSignatureSyntax
│ │ │ ├─parameterClause: FunctionParameterClauseSyntax
│ │ │ │ ├─leftParen: leftParen
│ │ │ │ ├─parameters: FunctionParameterListSyntax
│ │ │ │ │ ╰─[0]: FunctionParameterSyntax
│ │ │ │ │ ├─attributes: AttributeListSyntax
│ │ │ │ │ ├─modifiers: DeclModifierListSyntax
│ │ │ │ │ ├─firstName: wildcard
│ │ │ │ │ ├─secondName: identifier("model")
│ │ │ │ │ ├─colon: colon
│ │ │ │ │ ╰─type: IdentifierTypeSyntax
│ │ │ │ │ ╰─name: identifier("SampleModel")
│ │ │ │ ╰─rightParen: rightParen
│ │ │ ╰─effectSpecifiers: FunctionEffectSpecifiersSyntax
│ │ │ ╰─throwsClause: ThrowsClauseSyntax
│ │ │ ╰─throwsSpecifier: keyword(SwiftSyntax.Keyword.throws)
│ │ ╰─body: CodeBlockSyntax
│ │ ├─leftBrace: leftBrace
│ │ ├─statements: CodeBlockItemListSyntax
│ │ │ ├─[0]: CodeBlockItemSyntax
│ │ │ │ ╰─item: SequenceExprSyntax
│ │ │ │ ╰─elements: ExprListSyntax
│ │ │ │ ├─[0]: DeclReferenceExprSyntax
│ │ │ │ │ ╰─baseName: identifier("id")
│ │ │ │ ├─[1]: AssignmentExprSyntax
│ │ │ │ │ ╰─equal: equal
│ │ │ │ ╰─[2]: MemberAccessExprSyntax
│ │ │ │ ├─base: DeclReferenceExprSyntax
│ │ │ │ │ ╰─baseName: identifier("model")
│ │ │ │ ├─period: period
│ │ │ │ ╰─declName: DeclReferenceExprSyntax
│ │ │ │ ╰─baseName: identifier("id")
│ │ │ ╰─[1]: CodeBlockItemSyntax
│ │ │ ╰─item: SequenceExprSyntax
│ │ │ ╰─elements: ExprListSyntax
│ │ │ ├─[0]: DeclReferenceExprSyntax
│ │ │ │ ╰─baseName: identifier("text")
│ │ │ ├─[1]: AssignmentExprSyntax
│ │ │ │ ╰─equal: equal
│ │ │ ╰─[2]: MemberAccessExprSyntax
│ │ │ ├─base: DeclReferenceExprSyntax
│ │ │ │ ╰─baseName: identifier("model")
│ │ │ ├─period: period
│ │ │ ╰─declName: DeclReferenceExprSyntax
│ │ │ ╰─baseName: identifier("text")
│ │ ╰─rightBrace: rightBrace
│ ╰─[2]: ...
╰─rightBrace: rightBrace
イニシャライザの実装では、マクロが付与されている宣言内のプロパティ名と model の型が必要になります。
model の型はtypealiasの実装の時にnodeから取得できていますが、プロパティ名の取得についてはまだ取得できていないので、取得方法について考える必要があります。
init(_ model: SampleModel) throws {
id = model.id
text = model.text
}
プロパティ名を取得するためには、マクロを付与した宣言のツリー内容の情報が必要になります。
マクロを付与した宣言は、expansionメソッドのパラメーターのdeclarationに格納されています。
例に漏れずSyntaxProtocolに適合した型なので、今までと同じように lldb コマンドでツリー情報を確認できます。
`declaration`のツリー内容
StructDeclSyntax
├─attributes: AttributeListSyntax
│ ╰─[0]: AttributeSyntax
│ ├─atSign: atSign
│ ├─attributeName: IdentifierTypeSyntax
│ │ ╰─name: identifier("ModelConvert")
│ ├─leftParen: leftParen
│ ├─arguments: LabeledExprListSyntax
│ │ ╰─[0]: LabeledExprSyntax
│ │ ╰─expression: MemberAccessExprSyntax
│ │ ├─base: DeclReferenceExprSyntax
│ │ │ ╰─baseName: identifier("SampleModel")
│ │ ├─period: period
│ │ ╰─declName: DeclReferenceExprSyntax
│ │ ╰─baseName: keyword(SwiftSyntax.Keyword.self)
│ ╰─rightParen: rightParen
├─modifiers: DeclModifierListSyntax
├─structKeyword: keyword(SwiftSyntax.Keyword.struct)
├─name: identifier("SampleData")
╰─memberBlock: MemberBlockSyntax
├─leftBrace: leftBrace
├─members: MemberBlockItemListSyntax
│ ├─[0]: MemberBlockItemSyntax
│ │ ╰─decl: VariableDeclSyntax
│ │ ├─attributes: AttributeListSyntax
│ │ ├─modifiers: DeclModifierListSyntax
│ │ ├─bindingSpecifier: keyword(SwiftSyntax.Keyword.let)
│ │ ╰─bindings: PatternBindingListSyntax
│ │ ╰─[0]: PatternBindingSyntax
│ │ ├─pattern: IdentifierPatternSyntax
│ │ │ ╰─identifier: identifier("id")
│ │ ╰─typeAnnotation: TypeAnnotationSyntax
│ │ ├─colon: colon
│ │ ╰─type: IdentifierTypeSyntax
│ │ ╰─name: identifier("String")
│ ╰─[1]: MemberBlockItemSyntax
│ ╰─decl: VariableDeclSyntax
│ ├─attributes: AttributeListSyntax
│ ├─modifiers: DeclModifierListSyntax
│ ├─bindingSpecifier: keyword(SwiftSyntax.Keyword.let)
│ ╰─bindings: PatternBindingListSyntax
│ ╰─[0]: PatternBindingSyntax
│ ├─pattern: IdentifierPatternSyntax
│ │ ╰─identifier: identifier("text")
│ ╰─typeAnnotation: TypeAnnotationSyntax
│ ├─colon: colon
│ ╰─type: IdentifierTypeSyntax
│ ╰─name: identifier("String")
╰─rightBrace: rightBrace
StructDeclSyntax->memberBlock->membersの要素の中にプロパティの宣言があることがわかりました。
その中でもdeclの型がVariableDeclSyntaxのものがプロパティになり、その配下のpatternにプロパティ名が格納されているので、マクロの引数を取得した要領で、プロパティ名を全て取得します。
ここからプロパティの部分を探すのはやや大変なので、こういう時もSwift AST Explorerを使用するのがおすすめです。
Swift AST Explorer でもコードのツリー内容を見ることができ、ツリーにカーソルを当てると、どのツリーの要素がコードのどの部分に対応しているかがハイライトでわかるので、探している要素がわからないときは、このツールを使えば大体わかります。

let members: [IdentifierPatternSyntax] = declaration.memberBlock.members.compactMap {
guard let variable = $0.decl.as(VariableDeclSyntax.self),
let pattern = variable.bindings.first?.pattern.as(IdentifierPatternSyntax.self) else { return nil }
return pattern
}
membersの要素からVariableDeclSyntaxのものだけを選んでプロパティ名へ変換することで、プロパティ名の配列を取得しています。
あとは先ほどと同じ要領で、ツリーの内容通りにtypealiasのコードを SwiftSyntax で追加していきます。
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
let modelTypeToken = node.arguments!.cast(LabeledExprListSyntax.self).first!
.expression.cast(MemberAccessExprSyntax.self)
.base?.cast(DeclReferenceExprSyntax.self)
.baseName
let members: [IdentifierPatternSyntax] = declaration.memberBlock.members.compactMap {
guard let variable = $0.decl.as(VariableDeclSyntax.self),
let pattern = variable.bindings.first?.pattern.as(IdentifierPatternSyntax.self) else { return nil }
return pattern
}
return [.init(
extensionKeyword: .keyword(.extension),
extendedType: type,
inheritanceClause: .init(
colon: .colonToken(),
inheritedTypes: .init(itemsBuilder: {
InheritedTypeSyntax(
type: IdentifierTypeSyntax(
name: .identifier("ModelConvertible")
)
)
})
),
memberBlock: .init(members: .init(itemsBuilder: {
MemberBlockItemSyntax(
decl: TypeAliasDeclSyntax(
name: .identifier("ModelType"),
initializer: TypeInitializerClauseSyntax(
value: IdentifierTypeSyntax(name: modelTypeToken!)
)
)
)
MemberBlockItemSyntax(
decl: InitializerDeclSyntax(
signature: FunctionSignatureSyntax(
parameterClause: FunctionParameterClauseSyntax(
parametersBuilder: {
FunctionParameterSyntax(
firstName: .wildcardToken(),
secondName: .identifier("model"),
type: IdentifierTypeSyntax(name: modelTypeToken!)
)
}
),
effectSpecifiers: FunctionEffectSpecifiersSyntax(
throwsClause: ThrowsClauseSyntax(
throwsSpecifier: .keyword(.throws)
)
)
),
body: CodeBlockSyntax(
statements: CodeBlockItemListSyntax(itemsBuilder: {
members.map { variablePatter in
SequenceExprSyntax(
elements: ExprListSyntax(itemsBuilder: {
DeclReferenceExprSyntax(
baseName: variablePatter.identifier
)
AssignmentExprSyntax()
MemberAccessExprSyntax(
base: DeclReferenceExprSyntax(
baseName: .identifier("model")
),
declName: DeclReferenceExprSyntax(
baseName: variablePatter.identifier
)
)
})
)
}
})
)
)
)
}))
)]
}
現状のマクロで、動作確認をしてみると以下のようにイニシャライザが自動生成されるようになっています。

最後にツリー内容としては以下の部分でtoModelメソッドの実装を追加します。
ExtensionDeclSyntax
├─...
╰─memberBlock: MemberBlockSyntax
├─leftBrace: leftBrace
├─members: MemberBlockItemListSyntax
│ ├─...
│ ╰─[2]: MemberBlockItemSyntax
│ ╰─decl: FunctionDeclSyntax
│ ├─attributes: AttributeListSyntax
│ ├─modifiers: DeclModifierListSyntax
│ ├─funcKeyword: keyword(SwiftSyntax.Keyword.func)
│ ├─name: identifier("toModel")
│ ├─signature: FunctionSignatureSyntax
│ │ ├─parameterClause: FunctionParameterClauseSyntax
│ │ │ ├─leftParen: leftParen
│ │ │ ├─parameters: FunctionParameterListSyntax
│ │ │ ╰─rightParen: rightParen
│ │ ├─effectSpecifiers: FunctionEffectSpecifiersSyntax
│ │ │ ╰─throwsClause: ThrowsClauseSyntax
│ │ │ ╰─throwsSpecifier: keyword(SwiftSyntax.Keyword.throws)
│ │ ╰─returnClause: ReturnClauseSyntax
│ │ ├─arrow: arrow
│ │ ╰─type: IdentifierTypeSyntax
│ │ ╰─name: identifier("SampleModel")
│ ╰─body: CodeBlockSyntax
│ ├─leftBrace: leftBrace
│ ├─statements: CodeBlockItemListSyntax
│ │ ╰─[0]: CodeBlockItemSyntax
│ │ ╰─item: ReturnStmtSyntax
│ │ ├─returnKeyword: keyword(SwiftSyntax.Keyword.return)
│ │ ╰─expression: FunctionCallExprSyntax
│ │ ├─calledExpression: MemberAccessExprSyntax
│ │ │ ├─period: period
│ │ │ ╰─declName: DeclReferenceExprSyntax
│ │ │ ╰─baseName: keyword(SwiftSyntax.Keyword.init)
│ │ ├─leftParen: leftParen
│ │ ├─arguments: LabeledExprListSyntax
│ │ │ ├─[0]: LabeledExprSyntax
│ │ │ │ ├─label: identifier("id")
│ │ │ │ ├─colon: colon
│ │ │ │ ├─expression: DeclReferenceExprSyntax
│ │ │ │ │ ╰─baseName: identifier("id")
│ │ │ │ ╰─trailingComma: comma
│ │ │ ╰─[1]: LabeledExprSyntax
│ │ │ ├─label: identifier("text")
│ │ │ ├─colon: colon
│ │ │ ╰─expression: DeclReferenceExprSyntax
│ │ │ ╰─baseName: identifier("text")
│ │ ├─rightParen: rightParen
│ │ ╰─additionalTrailingClosures: MultipleTrailingClosureElementListSyntax
│ ╰─rightBrace: rightBrace
╰─rightBrace: rightBrace
こちらに関しては、今までの実装と同じ考えで実装できるので、実装内容の詳細は割愛します。
完成系は以下の通りです。
完成系のexpansionメソッド
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
let modelTypeToken = node.arguments!.cast(LabeledExprListSyntax.self).first!
.expression.cast(MemberAccessExprSyntax.self)
.base?.cast(DeclReferenceExprSyntax.self)
.baseName
let members: [IdentifierPatternSyntax] = declaration.memberBlock.members.compactMap {
guard let variable = $0.decl.as(VariableDeclSyntax.self),
let pattern = variable.bindings.first?.pattern.as(IdentifierPatternSyntax.self) else { return nil }
return pattern
}
return [.init(
extensionKeyword: .keyword(.extension),
extendedType: type,
inheritanceClause: .init(
colon: .colonToken(),
inheritedTypes: .init(itemsBuilder: {
InheritedTypeSyntax(
type: IdentifierTypeSyntax(
name: .identifier("ModelConvertible")
)
)
})
),
memberBlock: .init(members: .init(itemsBuilder: {
MemberBlockItemSyntax(
decl: TypeAliasDeclSyntax(
name: .identifier("ModelType"),
initializer: TypeInitializerClauseSyntax(
value: IdentifierTypeSyntax(name: modelTypeToken!)
)
)
)
MemberBlockItemSyntax(
decl: InitializerDeclSyntax(
signature: FunctionSignatureSyntax(
parameterClause: FunctionParameterClauseSyntax(
parametersBuilder: {
FunctionParameterSyntax(
firstName: .wildcardToken(),
secondName: .identifier("model"),
type: IdentifierTypeSyntax(name: modelTypeToken!)
)
}
),
effectSpecifiers: FunctionEffectSpecifiersSyntax(
throwsClause: ThrowsClauseSyntax(
throwsSpecifier: .keyword(.throws)
)
)
),
body: CodeBlockSyntax(
statements: CodeBlockItemListSyntax(itemsBuilder: {
members.map { variablePatter in
SequenceExprSyntax(
elements: ExprListSyntax(itemsBuilder: {
DeclReferenceExprSyntax(
baseName: variablePatter.identifier
)
AssignmentExprSyntax()
MemberAccessExprSyntax(
base: DeclReferenceExprSyntax(
baseName: .identifier("model")
),
declName: DeclReferenceExprSyntax(
baseName: variablePatter.identifier
)
)
})
)
}
})
)
)
)
MemberBlockItemSyntax(
decl: FunctionDeclSyntax(
name: .identifier("toModel"),
signature: FunctionSignatureSyntax(
parameterClause: FunctionParameterClauseSyntax(parameters: []),
effectSpecifiers: FunctionEffectSpecifiersSyntax(
throwsClause: ThrowsClauseSyntax(
throwsSpecifier: .keyword(.throws)
)
),
returnClause: ReturnClauseSyntax(
type: IdentifierTypeSyntax(name: modelTypeToken!)
)
),
body: CodeBlockSyntax(statementsBuilder: {
CodeBlockItemSyntax(
item: .init(
ReturnStmtSyntax(
expression: FunctionCallExprSyntax(
calledExpression: MemberAccessExprSyntax(
declName: DeclReferenceExprSyntax(
baseName: .keyword(.`init`)
)
),
leftParen: .leftParenToken(),
arguments: LabeledExprListSyntax(itemsBuilder: {
members.enumerated().map { index, variablePattern in
LabeledExprSyntax(
label: variablePattern.identifier,
colon: .colonToken(),
expression: DeclReferenceExprSyntax(
baseName: variablePattern.identifier
),
trailingComma: index + 1 >= members.count ? nil : .commaToken()
)
}
}),
rightParen: .rightParenToken()
)
)
)
)
})
)
)
}))
)]
}
以下のように実際のコードにマクロを付与してみると、想定通りのextensionが自動生成されている。

また、実装したテストコードも通るようになっていると思います!
おわり
いろいろ書いていたら、結構長くなってしましたが、これでおわりです!
ここまで見てきて、一見難しそうに見えるけど、ツリーの内容がわかれば簡単に実装でき、SwiftSyntax の使い方自体はシンプルだということが個人的には思えたので、それをうまく伝えられていたら嬉しいです。
とはいえ今回実装したサンプルのマクロは、実は穴だらけです。
例えば、以下のようにSampleModelの型を任意の型にネストさせると、マクロはエラーを起こしてしまいます。
@modelConvert(Hoge.SampleModel.self)
struct SampleData {
let id: String
let text: String
}
enum Hoge {
struct SampleModel: Sendable {
let id: String
let text: String
}
}
このように、どんなコードに対しても対応できるようにするには、もっと複雑なマクロの実装が必要になります。
今回のサンプルマクロ実装を通して、Swift macros に興味が出たらさらに凝ったマクロの実装を検討すると面白いかもしれません。
今回のサンプルマクロの実装はGithubでも公開しているので、興味があればぜひご確認ください。
※参考