kateinoigakukunのブログ

思考垂れ流しObserver

IndexStoreを使ってSwiftコードを静的解析する

Swiftで静的解析ツールを作るとなると、基本的にSwiftSyntaxを使うことが多いと思います。 ただ、SwiftSyntaxで得られる情報はコンパイラ内部のパイプラインの序盤で生成されるため、「ある変数がどこから参照されているか」といった意味解析後に解る情報は使えません。

そこで、Xcodeがコードジャンプやリネームなどに使っている(だろう)Index-While-Buildingという仕組みを紹介します。

Index-While-Buildingとはその名の通りコンパイラコンパイル中に生成する中間情報をIDEのコードインデックスに利用する、という仕組みです。 swiftcclangには-index-store-pathというオプションがありディレクトリを指定することで、そこにインデックスデータが出力されます。

この仕組で生成されたデータは、libIndexStoreを使って読み取ることができます。 libIndexStoreAppleがフォークするLLVMプロジェクトの一部で、Xcodeのツールチェーンに同梱されています。

詳しい話はこちらのドキュメントを参照してください。

docs.google.com

libIndexStoreの仕組みを使えば、SwiftSyntaxの構文レベルでの解析では実現できなかった、よりリッチな解析が可能になります。

libIndexStoreを使った例には以下のようなものがあります。

しかし、いざlibIndexStoreを使ってツールを作ろうとすると、Swift向けlibIndexStoreクライアントが無いことに気が付きます。上で紹介したツール達はそれぞれ内部でlibIndexStoreとのバインディングを自前実装しています。 indexstore-dbは一応IndexStoreクライアントとして使うこともできますが、単にIndexStoreを使うためにはtoo muchかつsourcekit-lspから使われる前提でAPIが公開されているため制約が多いです。

そこで、(比較的)libIndexStoreのインターフェースをそのままSwiftから使うためのクライアントライブラリを書きました。

github.com

そして、そのライブラリを使って実装したのがswift-renamerです。雑に紹介するとXcodeのリネーム機能をプログラマブルに制御するようなライブラリです。

github.com

こんな感じのコードを書くと

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 

これを使って便利なツールを作ってくれ〜