正規表現パッケージのコンパイルを何度も呼び出していないかチェックするlinterを作った。
TL;DR
- regexpパッケージのコンパイル処理はプロセス初期化時などに一度だけ行うのが望ましい
- https://golang.org/pkg/regexp
regexp.MustCompile
など
- これをチェックするlinterを作った
gostaticanalysis/analysisutil
を使えばすぐできた- SSAは難しいけどおもしろい!
regexpパッケージをコンパイルを使うときのお作法
Goの正規表現を使いたいときはregexp
パッケージを使う。
このパッケージの使い方には注意すべき点がある。
上記記事の以下の部分が注意点だ。
正規表現オブジェクトのコンパイルはコストのかかる処理であり、これは以下のようにグローバル領域にコンパイル済みの状態で宣言します。 検査時に随時生成することは推奨されません。
たとえば、WebサーバをGoで実装していた場合、リクエストを受け取るたびにコンパイル処理をしているとパフォーマンスが悪い。
func myHandler(w http.ResponseWriter, r *http.Request) {
// リクエストを受け取るたびに正規表現のコンパイルが走る。
re := regexp.MustCompile(`(gopher){2}`)
body, _ := ioutil.ReadAll(resp.Body)
if re.Match(re) {
// Do anything...
}
// Do anything...
}
正しくは次のように書かないといけない。
// グローバルスコープならばmain goroutin起動時に一度だけの実行で済む。
var re = regexp.MustCompile(`(gopher){2}`)
func myHandler(w http.ResponseWriter, r *http.Request) {
body, _ := ioutil.ReadAll(resp.Body)
if re.Match(re) {
// Do anything...
}
// Do anything...
}
このようなことを静的解析で指摘する静的解析ツールを作った。
と、言っても大部分のロジックは@tenntennさんのcalled linterを踏襲している。
「明らかに一度しか実行されない関数」は正しい利用方法だが、検知できない。
そのため誤検知してしまう部分にたいしてはコメントをつけることで無視する機能もつけた(流用させてもらった)。
func f() {
// lint:ignore regexponce allowed
validID = regexp.MustCompile(`^[a-z]+\[[0-9]+\]$`) // OK because add specified comment.
}
メインのロジックはanalysisyutilパッケージを使い、作り始めはskeletonコマンドでガッと作っている。便利。
自分で頑張った独自性というところだと、単純にコード内に現れたMuxtCompile
関数などをすべてエラーとせずに次の例外を設けている。
- グローバルスコープ
main
関数- ただし、
main
関数の中のfor
ループ内で使っていた場合はエラーにする。
- ただし、
init
関数
どれも起動時に一度しか実行されない部分なので、静的解析のエラーにはしない。
難しかったところ
標準pkgの関数だったので、最初はgo/importer.Default()
経由でregexp.MustCompile
などの*types.Func
オブジェクトを取得していた。
が、同じ関数に対するオブジェクトはずなのにコード中に現れるregexp.MustCompile
とはマッチしないようだ。pos
などの位置も全然違った。
実装を優先したのでちゃんと原因は調べられていない…
終わりに
SSAはなかなか難しい(私がデータ構造をきちんと理解できていない)ので、こちらの本を参考に見様見真似で書いた感じ。
もっと静的解析勉強して実装速度と実装できる手札を増やしていきたい。