🤖

Swift Macrosでスーパーシンプルなマクロを作る 付属型編

2024/05/01に公開

SUMMARY

Swift Macrosの概要を知りたいので、スーパーシンプルな付属型マクロを作って
ざっくりSwift Macrosを理解しようねって話。

今回作成したリポジトリがこちら👇
https://github.com/MrSmart00/PublicInitializeMacro/tree/main

はじめに

https://zenn.dev/nexearth/articles/401c8014e10a7f
☝️の記事の続きです。

前回は自立型マクロを作ったので、今回は付属型のマクロを作っていきます。

今回もSwift Macrosの概要を理解することを念頭に、スーパーシンプルなマクロを作っていこうと思います。

作りたいもの

structをpublicアクセサを付けた時に、コンストラクタが自動で生成されない件を解決したい。

🔻イメージこんな感じ

@PublicInit
public struct Hoge {
    let index: Int
    let text: String
}

こんな感じで定義すれば、publicの init() が自動で生成されて、別Packageから読み込むことができるようになりたい。

作りました

ということで作りました。
https://github.com/MrSmart00/PublicInitializeMacro

今回もアプリの中で作成、使用する想定で実装しました。
メインターゲットの中は今回の話とはあまり関係ないので、 packages の中の解説をしていきます。

Package Name 用途
App メインターゲットから呼ばれるアクセスポイントになるContentViewを持ってるPackage
Entity 今回生成するMacroを検証するために作成したEntity構造体を格納するPackage
このPackageの構造体を他のPackageで呼び出して使用する想定
Macros マクロの定義を置いておくPackage
Plugins マクロの実装ファイルを置いておくPackage

マクロ解説

今回も実装ファイルをPluginsに格納しています。

https://github.com/MrSmart00/PublicInitializeMacro/blob/e1cabc2cd3a04fe9eb06fafd0ed86f69703da5b8/packages/Sources/Plugins/PublicInitialization.swift#L14

作成したいマクロの性質上、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の値を見てコンストラクタ情報を生成したいので、
🔻のような感じで構造をパースします。
https://github.com/MrSmart00/PublicInitializeMacro/blob/e1cabc2cd3a04fe9eb06fafd0ed86f69703da5b8/packages/Sources/Plugins/PublicInitialization.swift#L39-L42

詳細なパース処理は別メソッドに分離していて

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だったらコンストラクタ処理からは除外)
上記の判定が全て通ったら parameterbody の二つのString値を持つSyntaxInfoに変換して、返却します。

全てのプロパティの解析が終わったら、SyntaxInfoのリストができるので、それを使って
コンストラクタの引数にするパラメータとコンストラクタの中に記述するBody要素を生成して行きます。

https://github.com/MrSmart00/PublicInitializeMacro/blob/e1cabc2cd3a04fe9eb06fafd0ed86f69703da5b8/packages/Sources/Plugins/PublicInitialization.swift#L43-L50

最後に出来上がったパラメータ文字列とBody要素を使ってInitializerDeclSyntaxにデータを流し込めば完成です 🎉

https://github.com/MrSmart00/PublicInitializeMacro/blob/e1cabc2cd3a04fe9eb06fafd0ed86f69703da5b8/packages/Sources/Plugins/PublicInitialization.swift#L52-L59

他にもSendableの対応とかも入れていますが、今回はその辺のお話は省略させてもらいます🙏

まとめ

Swift Syntaxに苦手意識があったので、最初は戸惑いしか無かったんですが、
慣れてくるとXMLのDOM操作に似てるなという印象です。
あと、マクロのユニットテスト環境が構築できたら開発が劇的に楽になるので、まずその辺の環境構築をするのが近道かなと思いました。

知れば知るほど活用の幅が増えるので、今後どんどん使っていこうと思います。
今回作成したリポジトリはこちらです。皆さんの参考になれば幸いです!

https://github.com/MrSmart00/PublicInitializeMacro/tree/main

Discussion