IndexStoreを使ってSwiftコードを静的解析する
Swiftで静的解析ツールを作るとなると、基本的にSwiftSyntaxを使うことが多いと思います。 ただ、SwiftSyntaxで得られる情報はコンパイラ内部のパイプラインの序盤で生成されるため、「ある変数がどこから参照されているか」といった意味解析後に解る情報は使えません。
そこで、Xcodeがコードジャンプやリネームなどに使っている(だろう)Index-While-Buildingという仕組みを紹介します。
Index-While-Buildingとはその名の通りコンパイラがコンパイル中に生成する中間情報をIDEのコードインデックスに利用する、という仕組みです。
swiftc
やclang
には-index-store-path
というオプションがありディレクトリを指定することで、そこにインデックスデータが出力されます。
この仕組で生成されたデータは、libIndexStore
を使って読み取ることができます。 libIndexStore
はAppleがフォークするLLVMプロジェクトの一部で、Xcodeのツールチェーンに同梱されています。
詳しい話はこちらのドキュメントを参照してください。
libIndexStoreの仕組みを使えば、SwiftSyntaxの構文レベルでの解析では実現できなかった、よりリッチな解析が可能になります。
libIndexStoreを使った例には以下のようなものがあります。
- apple/sourcekit-lsp: Swift向けLSP実装
- apple/indexstore-db: sourcekit-lsp向けのIndexStoreクライアント + クエリ高速化のためのDB。
- peripheryapp/periphery: 未使用コードを検出するツール
- apple/swift-package-manager: Swiftのパッケージング管理 + ビルドシステム
しかし、いざlibIndexStoreを使ってツールを作ろうとすると、Swift向けlibIndexStore
クライアントが無いことに気が付きます。上で紹介したツール達はそれぞれ内部でlibIndexStoreとのバインディングを自前実装しています。 indexstore-db
は一応IndexStoreクライアントとして使うこともできますが、単にIndexStoreを使うためにはtoo muchかつsourcekit-lspから使われる前提でAPIが公開されているため制約が多いです。
そこで、(比較的)libIndexStoreのインターフェースをそのままSwiftから使うためのクライアントライブラリを書きました。
そして、そのライブラリを使って実装したのがswift-renamerです。雑に紹介するとXcodeのリネーム機能をプログラマブルに制御するようなライブラリです。
こんな感じのコードを書くと
import SwiftRenamer let renamer = SwiftRenamer(storePath: indexStorePath) let replacements = try system.replacements(where: { (occ) -> String? in occ.symbol.usr == "s:16IntegrationTests9ViewModelC4nameSSSgvp" ? "nickname" : nil }) for (filePath, replacements) in replacements { let rewriter = try SourceRewriter(content: String(contentsOfFile: filePath)) replacements.forEach(rewriter.replace) let newContent = rewriter.apply() newContent.write(toFile: filePath, atomically: true, encoding: .utf8) }
こんな感じでリネームできます。これは「ある変数がどこから参照されているか」という情報をIndexStoreが持っているのでサクッと実装できます。
class User { - var name: String? + var nickname: String? } class ViewController: UIViewController { let user = User() override func viewDidLoad() { super.viewDidLoad() - user.name = "Initial Name" + user.nickname = "Initial Name" } }
IndexStoreにどんな情報が入っているのか気になった方は、swift-indexstoreをクローンしてindex-dump-tool
コマンドにXcodeが生成するIndexStoreディレクトリを渡すと中身が見えて楽しいと思います。
$ git clone https://github.com/kateinoigakukun/swift-indexstore $ cd swift-indexstore $ swift run index-dump-tool print-record --index-store-path ~/Library/Developer/Xcode/DerivedData/YourProject-XXXX/Index/DataStore
これを使って便利なツールを作ってくれ〜