Swift Macrosでスーパーシンプルなマクロを作る 付属型編
SUMMARY
Swift Macrosの概要を知りたいので、スーパーシンプルな付属型マクロを作って
ざっくりSwift Macrosを理解しようねって話。
今回作成したリポジトリがこちら👇
はじめに
☝️の記事の続きです。
前回は自立型マクロを作ったので、今回は付属型のマクロを作っていきます。
今回もSwift Macrosの概要を理解することを念頭に、スーパーシンプルなマクロを作っていこうと思います。
作りたいもの
structをpublicアクセサを付けた時に、コンストラクタが自動で生成されない件を解決したい。
🔻イメージこんな感じ
@PublicInit
public struct Hoge {
let index: Int
let text: String
}
こんな感じで定義すれば、publicの init()
が自動で生成されて、別Packageから読み込むことができるようになりたい。
作りました
ということで作りました。
今回もアプリの中で作成、使用する想定で実装しました。
メインターゲットの中は今回の話とはあまり関係ないので、 packages
の中の解説をしていきます。
Package Name | 用途 |
---|---|
App | メインターゲットから呼ばれるアクセスポイントになるContentViewを持ってるPackage |
Entity | 今回生成するMacroを検証するために作成したEntity構造体を格納するPackage このPackageの構造体を他のPackageで呼び出して使用する想定 |
Macros | マクロの定義を置いておくPackage |
Plugins | マクロの実装ファイルを置いておくPackage |
マクロ解説
今回も実装ファイルをPluginsに格納しています。
作成したいマクロの性質上、MemberMacroで大丈夫そうなので、MemberMacroに準拠したマクロを作っていきます。
ひとまず検証に必要な適当なデータを元に実装を進めていきます。
public struct Hoge {
public let index: Int
let text: String?
}
上記の検証用データを抽象構文木にすると🔻のような感じになるので、そのツリー情報を元に解析していくことになります。
Syntax
StructDeclSyntax
├─attributes: AttributeListSyntax
│ ╰─[0]: AttributeSyntax
│ ├─atSign: atSign
│ ╰─attributeName: IdentifierTypeSyntax
│ ╰─name: identifier("PublicInit")
├─modifiers: DeclModifierListSyntax
│ ╰─[0]: DeclModifierSyntax
│ ╰─name: keyword(SwiftSyntax.Keyword.public)
├─structKeyword: keyword(SwiftSyntax.Keyword.struct)
├─name: identifier("Hoge")
╰─memberBlock: MemberBlockSyntax
├─leftBrace: leftBrace
├─members: MemberBlockItemListSyntax
│ ├─[0]: MemberBlockItemSyntax
│ │ ╰─decl: VariableDeclSyntax
│ │ ├─attributes: AttributeListSyntax
│ │ ├─modifiers: DeclModifierListSyntax
│ │ │ ╰─[0]: DeclModifierSyntax
│ │ │ ╰─name: keyword(SwiftSyntax.Keyword.public)
│ │ ├─bindingSpecifier: keyword(SwiftSyntax.Keyword.let)
│ │ ╰─bindings: PatternBindingListSyntax
│ │ ╰─[0]: PatternBindingSyntax
│ │ ├─pattern: IdentifierPatternSyntax
│ │ │ ╰─identifier: identifier("index")
│ │ ╰─typeAnnotation: TypeAnnotationSyntax
│ │ ├─colon: colon
│ │ ╰─type: IdentifierTypeSyntax
│ │ ╰─name: identifier("Int")
│ ╰─[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: OptionalTypeSyntax
│ ├─wrappedType: IdentifierTypeSyntax
│ │ ╰─name: identifier("String")
│ ╰─questionMark: postfixQuestionMark
╰─rightBrace: rightBrace
memberBlock
の中のmembers
の値を見てコンストラクタ情報を生成したいので、
🔻のような感じで構造をパースします。
詳細なパース処理は別メソッドに分離していて
static func parse(_ item: MemberBlockItemSyntax) -> SyntaxInfo? {
if let syntax = item.decl.as(VariableDeclSyntax.self),
let pattern = syntax.bindings.first,
let identifier = pattern.pattern.as(IdentifierPatternSyntax.self)?.identifier,
let type = pattern.typeAnnotation,
!checkAccessors(pattern)
{
...
}
}
parseメソッドの最初の4行で各プロパティに接続していて、
最後に参照しているプロパティがComputed Propertyかの判定をしています。(Computad Propertyだったらコンストラクタ処理からは除外)
上記の判定が全て通ったら parameter
と body
の二つのString値を持つSyntaxInfoに変換して、返却します。
全てのプロパティの解析が終わったら、SyntaxInfoのリストができるので、それを使って
コンストラクタの引数にするパラメータとコンストラクタの中に記述するBody要素を生成して行きます。
最後に出来上がったパラメータ文字列とBody要素を使ってInitializerDeclSyntax
にデータを流し込めば完成です 🎉
他にもSendableの対応とかも入れていますが、今回はその辺のお話は省略させてもらいます🙏
まとめ
Swift Syntaxに苦手意識があったので、最初は戸惑いしか無かったんですが、
慣れてくるとXMLのDOM操作に似てるなという印象です。
あと、マクロのユニットテスト環境が構築できたら開発が劇的に楽になるので、まずその辺の環境構築をするのが近道かなと思いました。
知れば知るほど活用の幅が増えるので、今後どんどん使っていこうと思います。
今回作成したリポジトリはこちらです。皆さんの参考になれば幸いです!
Discussion