見出し画像

SwiftLintを使って[weak self]を検知した話

はじめに

一気に夏が来たのか、気がついたら半袖短パンのかむいです。
クロージャにおける循環参照の回避策である[weak self]を、Linterの力を借りて検知する仕組みをプロダクトに導入した話を書こうと思います。

経緯

クロージャを書く際、毎度のようにiOSエンジニアがお世話になるのがself句の循環参照問題ではないでしょうか。
循環参照によるメモリリークの恐れがあるため、内部でself句を必要とする場合には[weak self][unowned self]を記述する必要がありますが、うっかり記述漏れというのもよくあることです。

我々は普段そのような問題の対策としてLinterを用いており、iOS界隈におけるLinterの代表ツールといえばSwiftLintです。
しかしSwiftLintでも[weak self]検知は過去よりIssueとして存在しているものの、現在もOpenとなったまま仕組みが導入されてはいません。

そこでSwiftLintのcustom_rulesを使い、正規表現でクロージャ内部で[weak self]を書いていないself句を特定するアプローチを取ってみました。
こちらは以下の方が書かれた記事を基に実施したものとなります。

やったこと

まず上記記事を参考に.swiftlint.ymlに以下の様なcustom_ruleを追記します。


weak_self_closure:
 included: ".*.swift"
 name: "Use weak self in closures"
 message: "Use weak self when create a closure."
 regex: "\\{\\s*[^\\[]{0,50}\\s+\\bin\\b([a-zA-Z\\\\.\\(\\)\\[\\{\\}\\>\\/\\=]|\\s)+(self)[^!]
 severity: error

regexパラメータにて正規表現を使い、[weak self]を利用せずクロージャ内でself句を呼び出している箇所のチェックをしています。
エスケープ文字が入りとても見づらいですが、正規表現チェッカーを利用するとこのようなチェックをしていることがわかります。

https://www.debuggex.com/

しかしこれだとnon-escaping closure, DispatchQueue, UIView.animateなどの場合も指摘されることになってしまいます。そしてそれらを全て正規表現だけでチェックしきることに限界を感じます。

そこで以下の2案を考えてみました。
1. 特定の関数, Operator名のみ対象とするブロックリスト方式
2. 特定の関数, Operator名は対象外とするセーフリスト方式

当初は問題となる関数やOperatorの方が少ないと考え2.案を採用しようとしていました。しかし実際にはSwift標準のAPI(map, filter等), 実装した関数に生やしたescaping closure, OSSで提供されている関数でも相当な数が該当してしまう結果となりました。
そのため今回は1.案を採用し、sink, emit, drive, flatMapなどクロージャ内でself句を呼び出すケースが多い関数を対象とすることにしました。

完全に網羅することには至っておりませんが、結果としてプロジェクト内で定義している大多数の[weak self]の検出チェックが出来るようになりました。

ピピー![weak self]警察です👮‍♂️

今後の課題

今回の対応の問題として、網羅性だけでなくコメントアウトしている関数でも正規表現にチェックされるというものもあります。文字列チェックである以上、本来の静的解析とはまた別の部分で対応点が多いのもまた事実です。

文字列チェックではなく、改めてソースコードの静的解析による実現方法を考えると、swift-syntaxを使ったBuildToolPluginを実装があります。

解析結果を構文木として表現され、関数やModifierといったカテゴリ・文字列の特定が出来るそうです。
本来やりたかった形に近いものがこの手法のため、今回の対応でも課題がまた増えそうであればこちらにトライしてみようと思います。

REALITY iOSの静的解析のほか、REALITYの他領域の技術に興味がありましたら是非Meetyでお話ししてみませんか?お待ちしております!