Spoon(INRIA)を使ってプログラミング可能な静的コード解析を試す #java
Spoonは、プログラミング可能な静的コード解析&ソースコード変換ライブラリーです。
Spoon - Source Code Analysis and Transformation for Java
ここ数か月は、Javaの標準機能について書いたエントリーが続きました。
今回は、ちょっとマニアックなライブラリーについて書いてみます。
Spoon(INRIA)とは?
Spoon(INRIA)は、INRIA(フランス国立情報学自動制御研究所 - Wikipedia)で開発されている、静的コード解析&ソースコード変換ライブラリーです。
カッコでINRIAを付けているのは、別のSpoonというソフトウェア(英語版Wikipedia: Spoon (software)参照)があり、それと区別するためです*1。
以降は、単にSpoonと表記します。
Spoonは、独自の階層モデルを使ってコードを構文解析し、その結果をツリーで表現します。
解析を行うには、ツリーに対して探索を行うJavaプログラムを書いて実行します。
ソースコード変換も同様に、ツリーに対して挿入・更新・削除を行うJavaプログラムを書いて実行し、結果をソースコードとして出力することができます。
これを使うと、プロジェクトでの違反コードが見つかった場合にビルドを失敗させる、ということが可能になります。
既存の静的解析ツールでは、カスタムチェックルールを追加するのにバイトコードレベルの知識が必要になったりします。Spoonでは、バイトコードを意識しなくても良いので、比較的簡単にチェックコードを書くことができます。
リリースされているバイナリーは、target=java1.6
でコンパイルされているようです。そのため、実行にはJava6以上のJREが必要ですが、解析ターゲットは古いバージョンでも問題ないと思います。
あくまでランタイムはJava6ということなので、それ以外はJava8も使えます。SpoonのテストコードはJava8を使っていますし、もちろん、解析コードはJava8で書いてもOKです。
ロードマップ(下記リンク参照)によると、今のところサポートするJavaのバージョンはJava6までのようです。今後はJava7,Java8のサポートも目指しているみたいです。
spoon/ROADMAP.md at master · INRIA/spoon
Spoonは、CeCILL-Cという馴染みのないライセンスを使っています。フランスの法律を考慮したフランス版GNUのようなライセンスのようです。(CeCILL - Wikipedia)
準備
今回は、最新リリースバージョン4.1.0を使います。
Jarファイルを直接ダウンロードするには、下記リンク先のページからダウンロードしてください。スタンドアローンで使うのならspoon-core-4.1.0-jar-with-dependencies.jar
をダウンロードすると良いです。
Mavenで使うには、下記のdependencyを追加します。
<dependency> <groupId>fr.inria.gforge.spoon</groupId> <artifactId>spoon-core</artifactId> <version>4.1.0</version> </dependency>
使い方
GUIモードで実行してみます。
- 解析対象のサンプルコード(
target-src/local/SampleApp.java
)
package local; public final class SampleApp { public static void main(String[] args) { String s = "hello"; System.out.println(s); s = null; // 普通は やっちゃダメ System.exit(0); } }
コマンドラインで実行する例です。/path/to
はJarファイルのインストール先です。
$ java -jar /path/to/spoon-with-dependencies.jar -g -i target-src/
"-g"
オプションにより、GUIモードで起動します。
Mavenで実行する方法は、FAQページに記載されています。
IDEから実行する方が簡単かもしれません。私は最終的にEclipseで実行しています。
- 実行結果
ツリーは手動で展開しています。GUIの機能はそれほど充実していません。
解析のコードを書く
解析のコードを書いてSpoonで使うには、Processor
(Processor (Spoon Core 4.1.0-SNAPSHOT API)
)を実装します。
実行時には、Visitorパターンにより、各Processor
に各要素が渡されてきます。
先ほどのサンプルコードから、null
代入とSystem.out
検出してみましょう。
System.exit
の使用を検出するProcessor
(local.DetectingSystemExitProcessor
)
package local; import java.lang.reflect.Method; import spoon.processing.AbstractProcessor; import spoon.processing.Severity; import spoon.reflect.code.CtInvocation; import spoon.reflect.declaration.CtElement; import spoon.reflect.reference.CtExecutableReference; public final class DetectingSystemExitProcessor extends AbstractProcessor<CtInvocation<CtElement>> { static final Method system_exit = getDeclaredMethod(System.class, "exit", int.class); @Override public void process(CtInvocation<CtElement> element) { if (areSameMethods(element.getExecutable(), system_exit)) { getFactory().getEnvironment().report(this, Severity.ERROR, element, "System.exit(int)"); } } static Method getDeclaredMethod(Class<?> c, String methodName, Class<?>... argTypes) { try { return c.getDeclaredMethod(methodName, argTypes); } catch (NoSuchMethodException | SecurityException e) { throw new ExceptionInInitializerError(e); } } static boolean areSameMethods(CtExecutableReference<CtElement> executable, Method method) { if (executable != null) { Method actualMethod = executable.getActualMethod(); if (actualMethod != null) { return actualMethod.equals(method); } } return false; } }
null
代入を検出するProcessor
(local.DetectingSystemExitProcessor
)
package local; import spoon.processing.AbstractProcessor; import spoon.processing.Severity; import spoon.reflect.code.CtAssignment; import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtVariableAccess; public final class DetectingNullAssignmentProcessor extends AbstractProcessor<CtAssignment<CtVariableAccess<?>, ?>> { @Override public void process(CtAssignment<CtVariableAccess<?>, ?> element) { CtExpression<?> x = element.getAssignment(); if (x != null) { String sign = x.getSignature(); if ("null".equals(sign)) { // これで良いかどうか微妙 getFactory().getEnvironment().report(this, Severity.ERROR, element, "null assignment"); } } } }
それぞれ、ErrorとWarningとして検出させて、見つかった場合はその情報を標準出力に出力させています。
検出されたかどうかを判定するには、Launcher#getEnvironment
を使います。
Processor
の指定は、"-p"
オプションを使用します。2つのProcessor
を指定するには、セミコロンで区切ります。
$ java -jar /path/to/spoon-with-dependencies.jar -i target-src/ -p local.DetectingSystemExitProcessor;local.DetectingNullAssignmentProcessor
- 実行結果
2015-06-13 14:15:11,084 WARN spoon.support.StandardEnvironment - warning: System.exit(int) at local.SampleApp.main(SampleApp.java:9) 2015-06-13 14:15:11,085 ERROR spoon.support.StandardEnvironment - error: null assignment at local.SampleApp.main(SampleApp.java:8)
ソースコード変換(使い方だけ)
バイトコードを操作するのではなく、ツリーモデルに変換してからコードを挿入し、ツリーをソースコードに変換する方式のようです。
基本的な使い方は解析の場合と同じく、Processor
を実装します。解析はRead-onlyだったのに対して、ソースコード変換は要素への追加・変更・削除の操作を行います。
変換結果は、spooned/
という名前のディレクトリーに出力されます。なお、解析だけの場合でも出力されます。
サンプルは、下記リンク先ページの"Program Transformation"をご覧ください。
ざっとSpoonについてみてきました。
今後、新しいJavaバージョンへの追従がどこまで進むかに期待したいところです。
まだ全体を把握できていないので、比較的簡単そうなGUI部分を改造したPullReqでも送ってみようかなー、などと思っています。
(おわり)