nilawayの実装調査
が面白そうだったのと、やろうとして大変そうで放置していたことをまさにやっていて気になったのとで、実装を見てみる
とりあえずREADMEを確認した
以下あたりがポイントっぽそう
- 完全自動
→アノテーション必要なし - 速い
- 関数またいだnil panicは少なくとも一部は検出可能
- packageまたいでも問題なし
発生した疑問点
- 偽陽性は発生するのか?
- ルールベースの静的解析では常に偽陽性、偽陰性のトレードオフを考える必要がある
- 偽陽性(nil panicしないものへの警告)が多いとうるさくて使い物にならない
- 偽陰性(nil panicするものへ警告がない)と入れる意味が少ない
- 「実践的」の部分から以下は読み取れる
- 全てのnil panicを検出するわけではない
→偽陰性は発生する - 偽陽性については言及なし
→偽陽性が発生しうるのか不明
- 全てのnil panicを検出するわけではない
- ルールベースの静的解析では常に偽陽性、偽陰性のトレードオフを考える必要がある
- 関数をまたいだ場合、どこまで検出できるのか?
- 経験上、interfaceを通した場合や関数の変数への代入を挟んだ場合に検出が難しい
- interfaceを通した場合は特に、検出できないと結構偽陰性が増えてしまい検出率が落ちる
- ポインタ解析を使うとこれらも解決可能だが使えない
- ポインタ解析はinterfaceなどのポインタにどのような値が入るか解析する解析
- 計算コストが非常に高いため、「高速」をうたう以上おそらく使っていない
- 頑張って静的解析で対応しているなら、どのように対応しているか見れるとかなり面白いと思われる
- 経験上、interfaceを通した場合や関数の変数への代入を挟んだ場合に検出が難しい
For more detailed discussion, please check our paper.
https://github.com/uber-go/nilaway?tab=readme-ov-file#code-examples
のpaperに手法の説明が書いてありそうなので読みたいが、リンクがないので一旦放置…
wikiがあるので見ていく
本格的に内容があるのは以下2ページのみ(これも工事中っぽそう)
Architectureは実装周りの話。
大まかな仕組みは
- 各Analyserでtrigger?を作成
- triggerの補助情報を作成するAnalyzerもあるらしい
- Accumulation Analyzerがtriggerを基にinference algorithmを実行
仕組み面で気になるポイントは以下。
- Accumulation Analyzer
- inference algorithmの中身(おそらく最重要)
- Anonymous Function Analyzer
- 関数リテラル(=無名関数)の情報を収集
- 関数の変数代入に対応している期待が高くなる
- Affiliation Analyzer
- interface-struct affiliationのtrigger作成
- implementではなくaffiliation?
- implementのことなら型がinterfaceを実装しているかの判定になるので、すごい
- structのフィールドにinterfaceがあるか、みたいな話っぽくも聞こえる
- それはそれで使いどころが思いつかないので気になる
実装方法的に以下が面白そう
- エラー生成ロジックとエラー報告ロジックを切り離している
- Accumulation Analyzerは直で
analysis.Pass.Report
を使わない - 最上位のNilAway Analyzerが
analysis.Pass.Report
でエラー(おそらく警告のこと)の報告をする - これによって、必要ならエラーをファイルに書き出すとかが楽にできるらしい
- これをしたい場合があまり思いついていないけど…
-
analysis.Pass.Report
で雑に警告出せるのはanalysisパッケージのメリットだと思っていたので、ここを捨てるのが吉と出るか凶と出るか気になる
- Accumulation Analyzerは直で
- Config Analyzer
- flagからの設定の読み取りを専用analyzerに任せているの、なかなか見ない印象
- Analyzerを分割の単位として使っている
- 規模感的にGoの関数やパッケージで構成要素を切り分けても良い気がするが、Analyzerでコンポーネントを切り分けている
- これのメリット・デメリットは気になる
Configurationの方は、そこまで特筆するべきポイントはなさそう
書き忘れていたけど、説明がないAnnotation Analyzerがある当たり、必要になったら最終手段としてアノテーションで情報を追加する機能をつけることとかは考えているのかも?
これを考えると、Annotation Analyzerの役割も気になるポイント。
いよいよ実装見ていく
とりあえず、Analyzerとディレクトリの対応関係は以下になっていそう。
Analyzer | ディレクトリ |
---|---|
NilAway | / |
Accumulation | /accumulation |
Annotation | /annotation |
Config | /config |
その他 | /assertion/* |
ディレクトリ構造からもAccumulationは特別なanalyserで、そのほかのAnalyzerはそのための情報収集をしていることが読み取れる。
また、wikiにはなかったが、情報収集系AnalyzerをまとめるAssertion Analyzerが存在しているっぽい。
このAssertion AnalyzerからAnonymous Function AnalyzerとStruct Field Analyzerが参照されておらず、他からも参照されている気配がなかったので、どうやらこの2つは今のところ使われいないと思われる。
/assertion
の下の各Analyzerから見ていく。
まずは、Affiliation Analyzerから読む。
all affiliations (e.g., interface and its implementing struct)
実装関係とかのことをaffiliationと呼んでいたみたい。
Implementではなかったのは他の関係も含んでいるからと考えるのがよさそう。
楽しみになってきた。
Result
でannotation.FullTrigger
を返す。
これを見て気が付いたが、triggerが何か理解してからこのあたり見た方がよさそうなので、一旦切り上げて/annotation
見る。
FullTrigger
は基本的には「nilが発生する条件」と「発生したnilを使用する条件」の組と考えれば良さそう?
affilliationについてはここが本質っぽそう
代入とか関数呼び出しとかで「interfaceに他の方をいれる」状況が発生している箇所をもとに、implementの関係を取り出しているみたい
そして、affilliationの関係があれば、実装している型のメソッドで返り値などがnillableの場合、interfaceのメソッドも返り値がnilableになるtriggerを設定している