How to use JDT-AST

このページは,JDTを使ってソースコードの解析を行いたい人向けの情報を掲載しています.

What is JDT?

JDTとはJava development toolsの略で,Eclipse Foundationによってオープンソースソフトウェアとして提供されています.
基本的にEclipseのプラグインとして提供されるため,Eclipseプラグインプロジェクト以外から利用するにはコツが要ります.

Eclipse JDT Project

AST in JDT

JDTは,JavaのソースコードをAST(Abstract Syntax Tree)という中間表現にして保持します.
このASTをたどることによって,必要なソースコードの情報を得ることができます.
JDTのAST(以下,AST)では,Javaの各要素をASTNodeという型を継承したクラスにして格納しています.
たとえば,関数の定義はMethodDeclaration,return文はReturnStatementという型に格納されます.
これらASTNodeが階層的に組み合わされ,ASTとしてソースコードが表現されます.

JDTのソースコード中で,ASTに関するソースコードはorg.eclipse.jdt.core.domというパッケージにまとめられています.
基本的に,ドキュメントを見て各ノードがどのような要素を持っているのかを確認しながら進めることになります.
"MethodDeclaration"とかでググると,大体公式ドキュメントがヒットするのでそれを見ましょう.

公式ドキュメント中,Eclipse JDT API Specification.ASTを扱いたい場合には必須
ソースコードがどのようなASTになるのかを表示してくれる,便利なプラグイン - AST View

構築されたASTは,CompilationUnitというクラスになって返ってきます.
これはASTのルートノードで,もちろんASTNodeを継承しています.
簡単に言えば,1つのJavaファイルが1つのCompilationUnitとなります.

ASTをたどるには,Visitorパターンを使ってたどる必要があります.
ASTVisitorというクラスを継承したクラスを作り,解析したい要素をたどるよう処理を書きます.

How to build AST in Java project

Eclipseプラグインプロジェクトを使うときは,何も考えずドキュメント通りにコードを書くだけで正しく実行されます.
逆に,通常のJavaプロジェクトから利用する場合は,わりとめんどくさいです.
以下,通常のJavaプロジェクトについて記述します.

Pre-requirement

ASTを使うために,外部Jarを追加する必要があります.
必要なJarは以下です(*にはバージョンが入ります)

大量です.
これらは,Eclipseのインストールディレクトリ下,pluginsディレクトリにあるので,1つずつビルドパスに追加していきましょう.
再利用する場合めんどくさいので,必要なjarを1箇所にまとめておくと便利です.

Scala使いは,jface,jface.text,textを追加すると幸せになれます.

Code

ASTの構築

	// Create AST Parser
	ASTParser parser = ASTParser.newParser(AST.JLS4);
	parser.setSource(source.toCharArray());
	CompilationUnit unit = (CompilationUnit) parser.createAST(new NullProgressMonitor());
      

これだけです.

newParserに渡しているAST.JLS4はバージョンです.
全バージョンに対応してるそうなので,JDK8が出るまでは変えないでいいでしょう.

sourceは,ソースコードをString型の文字列に変換したものです.
よくある間違えとして,sourceにファイルパスを渡すというものがあります.
さらに,soruceはソースコード中の改行も正しく格納されている必要があります.

createASTで返ってくるCompilationUnitは,ASTのルートノードです.
1つのファイルを表しています.

ASTのVisit

	// Visit Node
	MyVisitor visitor = new MyVisitor();
	unit.accept(visitor);
      

MyVisitorは,ASTVisitorを継承したクラスです.
以下,MyVisitorのサンプルです.

	public class MyVisitor extends ASTVisitor {
	    @Override
   	    public boolean visit(MethodDeclaration node) {
	        // 処理
	        return super.visit(node);
	    }
	}
      

作りかけ

AST Level2

あまり知られていないですが,ASTには解析レベルがあります.
多くの場合では,レベル1の解析で十分ですが,詳細な解析を行いたい場合はレベル2を使う必要があります.
レベル2では変数やメソッドがどこで定義されているか(バインディング)が取得できるようになります.
つまり,同名の変数がある場合や,importされている先を見ないとどちらのメソッドが呼ばれているかわからない場合などなど,レベル1では自分で処理しないと取得できなかった情報をJDTが解析してASTに格納してくれます.

プラグインプロジェクトの場合,普通に使うと(ASTParser.setSourceにICompilationUnitなどを渡す)自動的にレベル2になります.
しかし,外部から使う場合はI~のクラスが作成できないです.

ちなみに,なぜI~が作成できないかというと,I~はIWorkspaceのインスタンスを持つ必要があるためです.
IWorkspaceは,Eclipseのワークスペースの情報が格納されているクラスです.
このような複数のソースコードをまとめる情報が無いとバインディングは解決できないです.
バインディングは複数のソースコードがあること前提なので.
つまり,ASTParser.setSource(char[] source)を使っている限りJDTはバインディングの対象としてみなしてくれないのです.

というわけで,レベル2のASTを構築するにはASTParser.createAST(IProgressMonitor monitor)を使わず,
void createASTs(String[] sourceFilePaths, String[] encodings, String[] bindingKeys, FileASTRequestor requestor, IProgressMonitor monitor)
を使う必要があります.

Techniques

要素の行番号を取得

CompilationUnitにgetLineNumber(int)というメソッドがあるので,これにASTNode.getStartPosition()の返り値を渡せば取得できる.

コメントを取得

JDTでは,コメントをAST上の特定要素の子として格納するのは悪としています.
そのため,JavaDoc以外のコメントはルートノードに独立して格納されます.
CompilationUnitにgetCommentListというメソッドがあるので,それを用いてコメントのリストを取得し,それぞれを処理する必要があります.
しかも珍妙なことに,visitorで処理したい場合には,CommentListの各要素に対してacceptをしないといけません.
以下,1つのvisitorにまとめた場合のサンプルコードです.
コメントだけ解析したい場合は,引用のリンクで書かれているようにすればいいでしょう.

How to access comments from the java compiler tree api generated ast?
	public class CommentVisitor extends ASTVisitor {

	CompilationUnit compilationUnit;
	private String[] source;

	public CommentVisitor(CompilationUnit compilationUnit, String[] source) {
          super();
          this.compilationUnit = compilationUnit;
          this.source = source;
	}

	public boolean visit(CompilationUnit node) {
	  for (Comment comment : (List) node.getCommentList()) {
            comment.accept(this(node, source));
          }
	}

	public boolean visit(LineComment node) {
          int startLineNumber = compilationUnit.getLineNumber(node.getStartPosition()) - 1;
          String lineComment = source[startLineNumber].trim();

          System.out.println(lineComment);

          return super.visit(node);
	}
	
	public boolean visit(BlockComment node) {
          int startLineNumber = compilationUnit.getLineNumber(node.getStartPosition()) - 1;
          int endLineNumber = compilationUnit.getLineNumber(node.getStartPosition() + node.getLength()) - 1;

          StringBuffer blockComment = new StringBuffer();

          for (int lineCount = startLineNumber ; lineCount<= endLineNumber; lineCount++) {
            String blockCommentLine = source[lineCount].trim();
	    blockComment.append(blockCommentLine);
	    if (lineCount != endLineNumber) {
	      blockComment.append("\n");
	    }
	  }

	  System.out.println(blockComment.toString());
	  return super.visit(node);
	}
     }
     

解析対象のバージョンを変える