本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはmomochiさんで、テーマは
<前回
動的解析による自動インポート
ここまでで、呼び出された関数のパッケージを静的解析し、必要なモジュールを自動でインポートできることがわかりました。しかし、my $v = somefunc()
のようにパッケージが指定されていないsomefunc
関数が呼び出されている場合に、インポートするべきモジュールを知るにはどうすればよいでしょうか。
一つの方法として、ライブラリでエクスポートされている関数を取得して、呼び出された関数から必要なモジュールをインポートする方法があります。具体的には、ライブラリのインクルードパスが格納されている@INC
からライブラリのファイルを1つずつ見ていき、Exporter
が参照する@EXPORT
変数を解析します。
この方法は、@EXPORT
にエクスポートする関数名が@EXPORT = qw(somefunc)
のようにハードコードされている場合にはうまくいきます。しかし、@EXPORT
は変数であり、右辺には文字列だけでなく関数も受けることができます。
Perlはコンパイルフェーズで振る舞いを変えることができる言語なので、ここに静的解析の限界があります。
本節では、エクスポートされている関数を動的に解析して必要なモジュールをインポートする方法について解説します。また、筆者が自動インポートモジュールを実装した際に気付いた注意点や課題を紹介します。
モジュールロードによる動的解析で自動インポートを実現
ライブラリ内でエクスポートされている関数をモジュールロードによる動的解析で取得し、パッケージが指定されていない関数から必要なモジュールをインポートします。
エクスポートされた関数を取得する
Exporter
が参照する変数を、評価後にアクセスできればエクスポートされた関数を漏れなく取得できます。Exporter
が参照している@EXPORT
変数はour
宣言されているので、シンボルテーブルにアクセスすることで値を取得できます。
たとえば、Exporter
によるインポート/ExModule
で定義されている@EXPORT
にアクセスするには次のようになります。ExModule
モジュールをロードするとExModule
で定義された@EXPORT
が評価され、シンボルテーブルに登録されてアクセス可能になります。
以上から、モジュールロードによって、エクスポートされた関数とモジュールのペアを取得できることがわかります。
パッケージが指定されていない関数から必要なモジュールをインポートする
静的解析によって、パッケージが指定されていない関数の候補を抽出できます。関数の候補は次の条件を満たすものとします。
- どこに
- 変数宣言、式、
sleep 1
といった文 - どのように
- 関数名は単語である
この条件を満たす関数の候補をPPI
の木構造を利用して抽出します。たとえば、sleep 1
の場合は文なのでPPI::Statement
を満たし、sleep
という関数名は単語なのでPPI::Token::Word
を満たすので、sleep
が抽出できます。
次に、ライブラリのインクルードパス以下のファイルに対して、モジュールロードによってエクスポートされた関数を取得します。たとえば、lib/
とエクスポートされている場合には、(Module::A, somefunc)
というモジュール名とエクスポートされた関数のペアを取得します。
@INC
に含まれるライブラリのインクルードパス以下のファイル名からModule::Load
モジュールでモジュールロードすることで、ライブラリファイルで定義された@EXPORT
変数にアクセスできます。よって、モジュール名とエクスポートされた関数のペアを取得できます。
ここまでで、関数の候補とライブラリでエクスポートされた関数の情報がわかっています。関数の候補がエクスポートされた関数名と一致していれば、対応するモジュールをインポートの候補にします。以降の、対象のファイルにインポートする方法は
以上から、パッケージ名を指定しない関数の呼び出しに対しても自動インポートを実現できました。
モジュールロード時に注意すべきこと
モジュールロードを行う際に注意するべきことは主に2つあります。
1つ目は、モジュールロードを正常に行うには普段の開発環境上で行う必要があることです。なぜなら、依存したモジュールがすべて存在し、モジュールを動作させるための設定が完了している環境上で行わないとエラーになるからです。この条件を満たすには、CPANモジュールとしてインストールできるようにします。
2つ目は、モジュールロード時に標準出力に出力されるとノイズになるので抑制する必要があることです。なぜなら、自動インポート後の出力に期待するのは入力のファイルに必要なuse
文を追加あるいは削除したものです。出力結果にノイズが含まれると、出力結果で入力したファイルを置換できなくなります。対策として、モジュールロード時にのみ標準出力を/dev/
にエイリアスすることでノイズを抑制できます。
動的解析の課題
動的解析はプログラムを実際に実行するのでセキュリティ上の課題があります。PerlではBEGIN
ブロックにコードを書くと、コンパイルフェーズの振る舞いを変えることができます。そのため、利用しているモジュールのBEGIN
ブロック内で悪意のあるコードが存在する場合に動的解析を行うと、悪意のあるコードが実行されます。たとえば、次のコードがモジュールに含まれていると、モジュールロード時に悪意のあるサイトにアクセスします。
動的解析を行う際は、Dockerなどの仮想環境上で実行することをお勧めします。
まとめ
今回解説したように、一見実装が難しそうに見える自動インポートでも、PPI
による静的解析とエッジケース用の動的解析を用いることで、実現可能だということがわかっていただけたかと思います。本稿で紹介した考え方を実装した一例として、筆者が製作しているPauというモジュールがあります。Pau
はgoimports
相当の機能を備えていて、必要なモジュールを自動で追加したり、不要なモジュールを自動で削除したりできます。良ければ参照してください。本稿が、Perlで自動インポートする際の考え方の参考になれば幸いです。
さて、次回の執筆者は須藤将史さんで、テーマは