高速なPythonの静的コード解析ツールを作った話
TL;DR
pylyzerというPython向けの静的コード解析ツールを開発した。
pylyzerは既存の静的解析ツール(e.g. pytype, pyright)よりも細かいコードの欠陥を指摘でき、エラー表示が丁寧であり、更に高速である。コードによっては100倍以上速く検査できた。
pylyzerはcargo(Rustのパッケージマネージャ)がインストールされている場合は以下のコマンドでインストール出来る。
cargo install pylyzer
pipでもインストール出来るが、現時点ではこの場合、標準ライブラリの型定義ファイルを得るためにErgというプログラミング言語を別途インストールする必要がある。
pip install pylyzer
# 追加でErgをインストール
curl -L https://github.com/mtshiba/ergup/raw/main/ergup.py | python3
開発のきっかけ
私はかねてよりErgというプログラミング言語を開発していました。
この言語はPython(のバイトコード)にトランスパイルされる静的型付け言語で、静的型付けの恩恵を受けながらPythonのコード資産を活用できる言語を標榜しているのですが、PythonのAPIをどうやって型付けするかという問題が開発当初より存在していました。Ergの型システムは静的ながらPythonの文法をほとんどカバーできるように設計されているので、型付けできるか自体は問題ではなく、それを誰がやるかが問題でした。というのも、ErgはTypeScriptなどと同じように型定義専用のファイル(d.er)があり、例えば以下のPythonスクリプトに対して
# foo.py
i = 1
def f(x): return x + 1
# foo.d.er
.i: Int
.f: (x: Int) -> Int # または一般化して.f: |A <: Add(Int)|(x: A) -> A.Output
というファイルを作ると、Erg側でこのスクリプトをimport出来るようになります。このファイルを逐一定義するのは中々骨な作業です。
これを自動化出来ないか?というのが、pylyzerの開発動機の一つでした。このツールはPythonに対する型検査を行い、型定義ファイルを生成します。勿論Pythonは動的型検査言語なので、厳密な静的型検査は出来ません。例えば、このようなスクリプトの検査は不可能です。
exec(input())
print(i) # iはコード中には定義されていないが、execで定義されるかもしれない。そしてその型は勿論不明
しかしこんなエキセントリックなコードというのは滅多に見かけるものではありません(ありませんよね......?)。逆に、動的型検査言語で書かれていても静的型付けできるコードというものも意外と多いのです。
def add(x, y):
return x + y
静的型付けと聞いてC言語の型付けなどを想像する方は、もしかしたらこのコードがコンパイル時に型付けできることに驚くかもしれません。
上のコードは以下のErgコードと等価です。
# add: |T: Type, A <: Add(T)|(x: T, y: A) -> A.Output
add x, y = x + y
コメントに書かれている型が自動で推論されます。
追記: Erg 0.6.7から構造型が導入されたため、addの型は以下のように推論される様になりました。これは2つの引数を持つメソッド
__add__
を定義するクラス全てを引数に取れることを意味します。add: |T, U, O, S <: Structural({.__add__ = (self: T, U) -> O})|(x: S, y: U) -> O
この関数はいわゆるジェネリックな関数です。関数の型の意味については説明すると長くなるので、気になる人はErgのドキュメントを読んでいただきたいのですが、このように型が全く明示的に出現しないコードでも静的型付けが可能ということは、Pythonのコードの静的型付けも意外とできる場合が多いのではないかと想像できましょう。まさにpylyzerはそのようなことをやっています。
pylyzerはPythonのASTをErgのASTに変換して、Ergの型検査器に突っ込みます。そして返ってきたエラーを適切に修正して表示します。
そしてリクエストがあればd.erファイルとしてPythonスクリプトの型定義を出力します。リクエストしなければ単なるPythonの静的解析ツールとして使用できますし、Ergと併用することでPythonからErgへのシームレスな連携が可能となる訳です。
更に、どうせならVSCodeなどのエディタと連携させたいということでLanguage serverの機能も実装して拡張機能にしました。
といっても既にErg用に実装したものを流用しているだけですが......
pylyzerの売り
1. 速い
冒頭も紹介した通り、pylyzerは既存のツールと比べてかなり高速に検査が出来る場合が多いです。
これは、pylyzerがRustで実装されているためであると考えます。他のツールは主にPythonやJavaScript(TypeScript)で実装されているようです。
2. 型を明示しなくてもかなり推論してくれる
既存のツールはPythonのtype hint機能を使って型が明示されているコードだけ検査するものが多いです。例えばmicrosoftの出しているpyrightなどがそうです。しかしpylyzerは可能な限り型推論してくれます。
3. 細かいところまで指摘してくれる
配列の境界外アクセスや辞書の存在しないキーへのアクセスなどもコンパイル時に検知すると警告してくれます。その他にも、使用されていない関数の戻り値など様々な警告オプションがあります。
4. エラー表示が丁寧
pyrightなどは主にLanguage serverとして実装されているためかエラー表示がかなり素っ気ないです。
pylyzerはしっかりとエラー箇所をコードとともに表示します。
終わりに
クリスマスに公開しようと巻きで開発したのでまだまだ荒削りですが、将来的には実用可能な解析ツールとなるよう鋭意開発していきますので、是非ともGitHubスターやバグ報告等のご支援をお願い致します。
Discussion