argius note

プログラミング関連

Spoon(INRIA)を使ってプログラミング可能な静的コード解析を試す #java

Spoonは、プログラミング可能な静的コード解析&ソースコード変換ライブラリーです。

Spoon - Source Code Analysis and Transformation for Java



ここ数か月は、Javaの標準機能について書いたエントリーが続きました。
今回は、ちょっとマニアックなライブラリーについて書いてみます。


目次

  • Spoon(INRIA)とは?
  • 準備
  • 使い方
  • 解析プログラムを書く
  • ソースコード変換(使い方だけ)



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で実行しています。


  • 実行結果

f:id:argius:20150613095853p:plain
図1:SpoonGUIモードでSampleApp.javaを解析したところ

ツリーは手動で展開しています。GUIの機能はそれほど充実していません。



解析のコードを書く

解析のコードを書いてSpoonで使うには、ProcessorProcessor (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でも送ってみようかなー、などと思っています。


(おわり)


*1:少なくとも英語圏ではSpoon (software)が指すのはこちらのSpoonではないという考え。