Compiler Tree API で Java ソースファイルをパースする

javax.tools と Compiler Tree API を使って Java のソースファイルをパースしてみました。

前回 と同じように、ソースファイルを AST 化した CompilationUnitTree (実際は JCTree$JCCompilationUnit) を取得し、簡単な TreeVisitor を適用します。

今回のソースは http://github.com/fits/try_samples/tree/master/blog/20150208/

はじめに

ソースファイルを CompilationUnitTree 化する一般的な方法が分からなかったので、とりあえず以下のような方法で試してみました。 (com.sun.tools.javac.main.JavaCompiler は Compiler Tree API ではありませんが)

  • (a) com.sun.tools.javac.main.JavaCompiler を利用
  • (b) com.sun.source.util.JavacTask を利用

今回は、取得した CompilationUnitTree へ下記の TreeVisitor 実装クラスを適用します。

SampleVisitor.java
import com.sun.source.util.TreeScanner;
import com.sun.source.tree.*;

public class SampleVisitor extends TreeScanner<Void, Void> {
    @Override
    public Void visitClass(ClassTree node, Void p) {
        // クラス名の出力
        System.out.println("class: " + node.getSimpleName());
        return super.visitClass(node, p);
    }

    @Override
    public Void visitMethod(MethodTree node, Void p) {
        // メソッド名の出力
        System.out.println("method: " + node.getName());
        return super.visitMethod(node, p);
    }

    @Override
    public Void visitLambdaExpression(LambdaExpressionTree node, Void p) {
        // ラムダ式の内容を出力
        System.out.println("lambda: " + node);
        return super.visitLambdaExpression(node, p);
    }
}

SampleVisitor は以下の内容を出力します。

(a) com.sun.tools.javac.main.JavaCompiler 利用

com.sun.tools.javac.main.JavaCompiler を使った処理内容は以下の通りです。

  • (1) javax.tools.ToolProvider から javax.tools.JavaCompiler 取得
  • (2) javax.tools.JavaCompiler から StandardJavaFileManager 取得
  • (3) StandardJavaFileManager から JavaFileObject (の Iterable) 取得
  • (4) JavaFileObject を com.sun.tools.javac.main.JavaCompiler で parse して CompilationUnitTree (JCTree$JCCompilationUnit) を取得
  • (5) CompilationUnitTree へ TreeVisitor を適用

javax.tools.JavaCompilercom.sun.tools.javac.main.JavaCompiler は別物なのでご注意ください。

StandardJavaFileManagergetJavaFileObjects メソッドはソースファイル名を可変長引数の文字列で与えれば JavaFileObject (の Iterable) を取得できるので、コマンドライン引数の args をそのまま渡しています。

JavaCompilerParse.java
import java.io.IOException;

import javax.tools.ToolProvider;
import javax.tools.StandardJavaFileManager;

import com.sun.source.tree.CompilationUnitTree;

import com.sun.tools.javac.main.JavaCompiler;
import com.sun.tools.javac.util.Context;

public class JavaCompilerParse {
    public static void main(String... args) throws IOException {
        try (
            // (1) (2)
            StandardJavaFileManager fileManager = 
                ToolProvider.getSystemJavaCompiler()
                    .getStandardFileManager(null, null, null)
        ) {
            JavaCompiler compiler = new JavaCompiler(new Context());

            // (3)
            fileManager.getJavaFileObjects(args).forEach(f -> {
                // (4) AST 取得
                CompilationUnitTree cu = compiler.parse(f);
                // (5) クラス・メソッド名とラムダ式の出力
                cu.accept(new SampleVisitor(), null);
            });
        }
    }
}

JDK の lib/tools.jar を CLASSPATH へ指定してコンパイルする必要があります。

コンパイル
> javac -cp %JAVA_HOME%/lib/tools.jar *.java

実行する際も lib/tools.jar が必要です。
今回は適当に用意した Sample.java をパースしてみました。

実行結果
> java -cp .;%JAVA_HOME%/lib/tools.jar JavaCompilerParse Sample.java

class: Sample
method: sample1
method: sample2
lambda: (n)->n * 2
class: SampleChild
method: child1
lambda: (n)->{
    System.out.println(n);
    return n > 0 && n % 2 == 0;
}
method: child2

使用した Sample.java の内容は以下の通りです。

Sample.java
import java.util.stream.IntStream;

class Sample {
    public String sample1() {
        return "sample";
    }

    public int sample2(int x) {
        return IntStream.range(0, x).map(n -> n * 2).sum();
    }

    class SampleChild {
        public void child1(int... nums) {
            IntStream.of(nums).filter(n -> {
                System.out.println(n);
                return n > 0 && n % 2 == 0;
            }).forEach(System.out::println);
        }

        private void child2() {
        }
    }
}

(b) com.sun.source.util.JavacTask 利用

com.sun.source.util.JavacTask には、CompilationUnitTree (の Iterable) を取得する parse メソッドが用意されていますが、JavacTask を取得する一般的な方法が分からなかったので、今回は javax.tools.JavaCompilergetTask メソッドで取得した JavaCompiler.CompilationTask の実装オブジェクト (com.sun.tools.javac.api.JavacTaskImpl) を JavacTask へキャストしました。

  • (1) javax.tools.ToolProvider から javax.tools.JavaCompiler 取得
  • (2) javax.tools.JavaCompiler から StandardJavaFileManager 取得
  • (3) javax.tools.JavaCompiler を使って StandardJavaFileManager と JavaFileObject (の Iterable) から JavacTask 取得
  • (4) JavacTask を parse して CompilationUnitTree (の Iterable) を取得
  • (5) CompilationUnitTree へ TreeVisitor を適用
JavacTaskParse.java
import java.io.IOException;

import javax.tools.ToolProvider;
import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;

import com.sun.source.util.JavacTask;

public class JavacTaskParse {
    public static void main(String... args) throws IOException {
        // (1)
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

        try (
            // (2)
            StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)
        ) {
            // (3)
            JavacTask task = (JavacTask)compiler.getTask(null, fileManager, null, null, null, fileManager.getJavaFileObjects(args));
            // (4) (5)
            task.parse().forEach(cu -> cu.accept(new SampleVisitor(), null));
        }
    }
}
実行結果
> java -cp .;%JAVA_HOME%/lib/tools.jar JavacTaskParse Sample.java

class: Sample
method: sample1
method: sample2
lambda: (n)->n * 2
class: SampleChild
method: child1
lambda: (n)->{
    System.out.println(n);
    return n > 0 && n % 2 == 0;
}
method: child2