注意書き
情報を集めたのが数年前なので、より新しい方法が存在する可能性があります。
対象者
- Javaのコードを簡単に解析したい
- Eclipseのプラグイン以外で解析したい
Eclipseのプラグインの一機能として解析を行う場合は、より簡単に行うことができます。
本ページでは、独立したツールとして解析を行う場合を対象とします。
背景
Eclipseとは?
Java使いならきっと使ったことのある、有名IDE。
ちなみに自分はIntelliJ IDEAに乗り換えています。
というより、Java書いてない……。
JDTとは?
Java Development Toolsの略で,Eclipse Foundationによってオープンソースソフトウェアとして提供されています.
基本的にEclipseのプラグインとして提供されるため,Eclipseプラグインプロジェクト以外から利用するにはコツが要ります.
JDTのAST
Abstract Syntax Treeの略で、ソースコードの中間表現です。
JDTでは、JavaのソースコードをASTに変換し保持します。
このASTをたどることによって、ソースコード中の必要な情報を得ることができます。
ASTの構成
JDTのASTでは、Javaの各要素をASTNodeという型を継承したクラスにして格納します。
たとえば、関数の定義はMethodDeclaration、return文はReturnStatementという型として格納されます。
構築されたASTは、CompilationUnitというクラスになって返ってきます。
これはASTのルートノードで、もちろんASTNodeを継承しています。
簡単に言えば、1つのJavaファイルが1つのCompilationUnitとなります。
調べ方
JDTのソースコード中で、ASTに関する処理はorg.eclipse.jdt.core.domというパッケージにまとめられています。
基本的に、ドキュメントを参照し、各ノードがどのような要素を持っているのかを確認しながら進めることになります。
"MethodDeclaration"とかでググると、大体公式ドキュメントがヒットするのでそれを見ましょう。
ソースコードがどのようなASTになるのかを表示してくれる、便利なプラグインもあります(AST View)
ASTの辿り方
ASTをたどるには、Visitorパターンを使ってたどる必要があります。
ASTVisitorを継承したクラスを作り、解析したい要素をたどるよう処理を書きます。
ASTを作る
では実際にASTを作ってみましょう。
外部Jarの追加
JDTを使うために、外部Jarを追加する必要があります。
必要なJarを以下に列挙します(*にはバージョンが入ります)。
- org.eclipse.core.contenttype_*.jar
- org.eclipse.core.jobs_*.jar
- org.eclipse.core.resources_*.jar
- org.eclipse.core.runtime_*.jar
- org.eclipse.equinox.common_*.jar
- org.eclipse.equinox_preferences_*.jar
- org.eclipse.jdt.core_*.jar
- org.eclipse.osgi_*.jar
大量ですね。
Eclipseのインストールディレクトリ下、pluginsディレクトリにあります。
1つずつビルドパスに追加していきましょう。
Scala使いは、jface、jface.text、textを追加する必要があるようです。
ASTを構築
// Create AST Parser
ASTParser parser = ASTParser.newParser(AST.JLS8);
parser.setSource(source.toCharArray());
CompilationUnit unit = (CompilationUnit) parser.createAST(new NullProgressMonitor());
これだけです。
newParserに渡しているAST.JLS8は、解析可能な言語バージョンです。
全バージョンに対応してるそうなので、新しい定義ができるまでは特に触れる必要はありません。
sourceはソースコードをString型の文字列に変換したものです。
ファイルパスを渡してはいけません。
さらに、soruceはソースコード中の改行も正しく格納されている必要があります。
createASTで返ってくるCompilationUnitは、ASTのルートノードです。
1つのファイルを表しています。
Visitorを書く
以下のようなASTVisitorを継承したVisitorを作成します。
public class MyVisitor extends ASTVisitor {
@Override
public boolean visit(MethodDeclaration node) {
// 処理
return super.visit(node);
}
}
作成したVisitorは、CompilationUnit.acceptに渡すことで、そのCompilationUnit用のVisitorとして登録されます。
// Visit Node
MyVisitor visitor = new MyVisitor();
unit.accept(visitor);
ここまでで、基本的な解析ができるようになりました。
MyVisitorに辿りたいASTNodeに対応したvisitメソッドを実装することで、簡単に各種情報を取得することができます。
Level 2のASTを作る
上記の方法だと、解析の際に有用ないくつかの情報が欠落してしまいます。
多くの場合では、上記の解析(Level 1)で十分ですが、詳細な解析を行いたい場合はLevel 2の解析を使う必要があります。
Level 2では、変数やメソッドがどこで定義されているか(バインディング)が取得できるようになります。
つまり、同名の変数がある場合や、importされている先を見ないとどちらのメソッドが呼ばれているかわからない場合など、Level 1では自分で処理しないと取得できなかった情報をJDTが解析し、ASTに格納してくれます。
プラグインプロジェクトの場合、普通に使うと(ASTParser.setSourceにICompilationUnitなどを渡す)自動的にLevel 2になります。
しかし、外部から使う場合はICompilationUnitのクラスが作成できず、取得できません。
なぜICompilationUnitが作成できないかというと、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)
を使う必要があります。
createASTsを使うサンプルを以下に示します。
final String[] sourcePathDirs = getSourcePathDirs();
final String[] classPaths = getLibraries();
final String[] sources = getListFiles();
final Map<String,String> options = JavaCore.getOptions();
JavaCore.setComplianceOptions(version.getVersion(), options);
parser.setCompilerOptions(options);
parser.setResolveBindings(true);
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setBindingsRecovery(true);
parser.setStatementsRecovery(true);
parser.setEnvironment(classPaths, sourcePathDirs, null, true);
String[] keys = new String[] {};
parser.createASTs(sources, null, keys, new ASTRequestor(), new NullProgressMonitor());
詳細は、マニュアルを確認してください。
備考
要素の行番号を取得する
CompilationUnitにgetLineNumber(int)というメソッドがあるので、これにASTNode.getStartPosition()の返り値を渡せば取得できます。
コメントを取得
JDTでは、コメントをAST上の特定要素の子として格納するのは悪とされています。
確かに、どのノードにくっつけたらいいかわからないですね。
そのため、JavaDoc以外のコメントはルートノードに独立したノードとして格納されます。
CompilationUnitにgetCommentListというメソッドがあるので、それを用いてコメントのリストを取得し、それぞれを処理する必要があります。
なお、通常のノードと独立しているせいか、getCommentListで取得したCommentクラスのそれぞれにacceptをしないといけません。
以下は、1つのvisitorを再帰的に利用し、コメントと通常のノードをまとめて辿れるようにした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<comment>) 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);
}
}
まとめ
JDTを使うと、比較的簡単にJavaのコードを解析することができます。
上記方法ではプラグインプロジェクト以外で使えるため、リファクタリングや研究用のツールに力を発揮するかと思います。