Eclipseのプラグイン開発のJDTのAST(abstract syntax tree・抽象構文木)について。
|
JDTのASTは、DOM(Document Object Model)と同じ方式のJavaソースの構文解析木。
IType等のクラスではJavaソースを変更することは出来ないが、ASTでは変更することが出来る。
ASTではASTNodeというクラスのツリーという形でソースの解析結果が保持される。
ICompilationUnitからASTParserを使ってASTNodeを取得することが出来る。
(ICompilationUnitはJavaソースファイルに当たるオブジェクト。IType等からgetCompilationUnit()で取得できる)
import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTParser;
public static ASTNode getASTNode(ICompilationUnit unit) {
ASTParser parser = ASTParser.newParser(AST.JLS4);
parser.setSource(unit);
ASTNode node = parser.createAST(new NullProgressMonitor());
return node;
}
特定の範囲だけでいいときは、その範囲に絞ることも出来る。
public static ASTNode getASTNode(IType type) {
ASTParser parser = ASTParser.newParser(AST.JLS4);
parser.setSource(type.getCompilationUnit());
ISourceRange range = type.getSourceRange();
parser.setSourceRange(range.getOffset(), range.getLength());
ASTNode node = parser.createAST(new NullProgressMonitor());
return node;
}
ASTNode内の各要素(フィールドやメソッド、メソッドの中の文やimport文なども!)をVisitorパターンで走査することが出来る。[2013-11-27]
メソッド名でメソッド定義のASTNode(MethodDeclaration)を検索する例。
(MethodDeclarationはASTNodeの具象クラス。メソッド定義を表す)
ASTNode node = 〜;
MethodFindVisitor visitor = new MethodFindVisitor("メソッド名");
node.accecpt(visitor);
MethodDeclaration method = visitor.getFoundMethod();
if (method != null) {
System.out.printf("Javadoc =%s%n", method.getJavadoc());
System.out.printf("可視性等 =%s%n", method.modifiers());
System.out.printf("戻り型 =%s%n", method.getReturnType2());
System.out.printf("メソッド名=%s%n", method.getName().getIdentifier());
System.out.printf("引数 =%s%n", method.parameters());
System.out.printf("本体 =%s%n", method.getBody());
}
import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.MethodDeclaration;
public class MethodFindVisitor extends ASTVisitor {
private String methodName;
private MethodDeclaration method;
public MethodFindVisitor(String methodName) {
this.methodName = methodName;
}
@Override
public boolean visit(MethodDeclaration node) {
String name = node.getName().getIdentifier();
if (name.equals(this.methodName)) {
this.method = node;
}
return false;
}
public MethodDeclaration getFoundMethod() {
return this.method;
}
}
Javaのクラス内の各要素に対応したvisitメソッドがあるので、自分が扱いたい引数のメソッドをオーバーライドする。
(メソッド定義を取得したい場合は、引数がMethodDeclarationのvisitメソッドをオーバーライドする)
visitメソッドの戻り値は、さらに内部を走査する場合はtrueを指定する。
例えばメソッドの場合、trueを返すとメソッド内の各ステートメントも走査されるが、falseを返すとメソッド内のステートメントは走査されない。
ASTNodeはソースファイル内の位置(offset)を持っているので、オフセットを指定した走査を行うことも出来る。[2014-02-22]
import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.MethodDeclaration;
public class OffsetVisitor extends ASTVisitor {
private final int offset;
public OffsetVisitor(int offset) {
this.offset = offset;
}
@Override
public boolean preVisit2(ASTNode node) {
int offset = node.getStartPosition();
int length = node.getLength();
return (offset <= this.offset) && (this.offset < offset + length);
}
〜
}
preVisit2()は共通の前処理を行うメソッド。(Eclipse3.5以降)
preVisit2()は各visitメソッドが呼ばれる前に呼ばれ、ここでtrueを返した場合だけ各クラスのvisitメソッドが呼ばれる。
クラスを表すTypeからFQCN(fully qualified
class name)を取得するには、resolveBinding()を使う。[2014-11-27]
(Typeそのものはソース上に書かれたクラス名(たいていは単純名)しか取得できない)
ただし、(単にresolveBinding()を呼び出すとnullが返ってくるので、)ASTParserに対してsetResolveBindings(true)を呼んでおく必要がある。
import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.Type;
ASTParser parser = ASTParser.newParser(AST.JLS4); parser.setSource(unit); parser.setResolveBindings(true); ASTNode node = parser.createAST(new NullProgressMonitor()); node.accept(new MyVisitor());
class MyVisitor extends ASTVisitor {
@Override
public boolean visit(SingleVariableDeclaration node) {
Type type = node.getType();
ITypeBinding bind = type.resolveBinding();
String fullyQualifiedClassName = bind.getQualifiedName();
// IType type = (IType) bind.getJavaElement();
〜
}
}
参考: ashigeruさんのJava/EclipseでDSLサポート (2) - コンパイルプロセスへの介入
ASTNodeを変更することにより、Javaソースコードを変更することが出来る。[2013-11-27]
import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.SimpleType; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.text.edits.TextEdit;
IDocument document = 〜; // 変更対象のソースのIDocument IType type = 〜; // 変更対象のクラス ASTParser parser = ASTParser.newParser(AST.JLS4); parser.setSource(type.getCompilationUnit()); parser.setKind(ASTParser.K_COMPILATION_UNIT); CompilationUnit unitNode = (CompilationUnit) parser.createAST(new NullProgressMonitor()); unitNode.recordModifications(); AST ast = unitNode.getAST(); MethodDeclaration method = 〜; // CompilationUnitから変更対象のメソッドを取得 modifyMethod(method); applyDocument(document, unitNode);
ちなみに、ICompilationUnitはJDTのインターフェース、CompilationUnitはdomのクラス。名前が似ているけど全然違うものなので注意。
private void modifyMethod(MethodDeclaration method) {
AST ast = method.getAST();
// メソッド名を「example」に変える
SimpleName name = ast.newSimpleName("example");
method.setName(name);
// 戻り型を「String」に変える
SimpleType rtype = ast.newSimpleType(ast.newName("String"));
d.setReturnType2(rtype);
}
ASTNodeからASTを取得する。
ASTのnewメソッドを呼び出して新しいASTNode(の具象クラス)を生成し、それを既存のASTNodeにセットすると変更することが出来る。
private void applyDocument(IDocument document, CompilationUnit unitNode) throws MalformedTreeException, BadLocationException {
// 変更箇所を生成する
TextEdit edit = unitNode.rewrite(document, null);
// 変更をドキュメントに反映する
edit.apply(document);
}
CompilationUnitからTextEdit(変更内容)を生成し、それをIDocumentに反映させる。
参考: ashigeruさんのInsertAssertionHandler.java
ASTNodeの各具象クラスはASTのnewメソッドを使って生成する。[2013-11-28]
import org.eclipse.jdt.core.dom.*;
プリミティブ型(int・longとかvoidとか)はPrimitiveTypeで扱う。[2013-11-28]
Type type = ast.newPrimitiveType(PrimitiveType.INT);
Type type = ast.newPrimitiveType(PrimitiveType.toCode("int"));
toCode()は、プリミティブ型でない文字列を指定した場合はnullを返すので注意。
単純なクラス(型引数を持たないクラス)はSimpleTypeで扱う。[2013-11-28]
Type type = ast.newSimpleType(ast.newName("java.lang.String"));
→クラス名(FQCN)を単純名に変換するにはImportRewriteを使う。
JavadocはJavadocで扱う。[2013-11-28]
// /** // * Javadocの実験 // * @param in 引数 // * @return 戻り値 // */ // というJavadocの例
Javadoc javadoc = ast.newJavadoc();
List<TagElement> tags = javadoc.tags();
{
TagElement tag = ast.newTagElement();
TextElement text = ast.newTextElement();
text.setText("Javadocの実験");
tag.fragments().add(text);
tags.add(tag);
}
{
TagElement tag = ast.newTagElement();
tag.setTagName(TagElement.TAG_PARAM); // @param
SimpleName name = ast.newSimpleName("in");
tag.fragments().add(name);
TextElement text = ast.newTextElement();
text.setText("引数");
tag.fragments().add(text);
tags.add(tag);
}
{
TagElement tag = ast.newTagElement();
tag.setTagName(TagElement.TAG_RETURN); // @return
TextElement text = ast.newTextElement();
text.setText("戻り値");
tag.fragments().add(text);
tags.add(tag);
}
「@param」や「@return」といったキーワードはTagElementに定数値が定義されている。[2014-05-10]
コメントオブジェクトもast.newLineComment()やast.newBlockComment()で作り出すことが出来るが、これらのコメントオブジェクトは参照専用らしく、これらを使ってコメントを追加することは出来ない。[2014-05-08]
(コメントそのものはCompilationUnitで保持されており、CompilationUnit#getCommentList()で取得できる)
ASTRewriteを使ってコメントを生成・追加することが出来る。
波括弧ブロック(メソッド本体等)の中にコメントを追加する例。
import org.eclipse.jdt.core.dom.Statement; import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
ASTRewrite rewriter = ASTRewrite.create(ast);
Block block = ast.newBlock();
List<Statement> slist = block.statements();
Statement placeHolder = (Statement) rewriter.createStringPlaceholder("//mycomment", ASTNode.EMPTY_STATEMENT);
slist.add(placeHolder);
参考: X WangさんのAdd Comments by using Eclipse JDT ASTRewrite
import文を表すImportDeclarationというクラスも他のASTNodeと同様に使える。[2013-11-28]
が、ソースコードを編集するという目的なら、ImportRewriteを使う方が便利。
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; import org.eclipse.jdt.ui.CodeStyleConfiguration;
IDocument document = 〜; // 変更対象のソースのIDocument IType type = 〜; // 変更対象のクラス ASTParser parser = ASTParser.newParser(AST.JLS4); parser.setSource(type.getCompilationUnit()); parser.setKind(ASTParser.K_COMPILATION_UNIT); CompilationUnit compilationUnit = (CompilationUnit) parser.createAST(null); compilationUnit.recordModifications(); AST ast = compilationUnit.getAST(); ImportRewrite importRewrite = CodeStyleConfiguration.createImportRewrite(compilationUnit, true); MethodDeclaration method = 〜; // CompilationUnitから変更対象のメソッドを取得 // クラス名を使用する(import文を追加する) Type rtype = ast.newSimpleType(ast.newName(importRewrite.addImport("com.example.MyClass"))); method.setReturnType2(rtype); // メソッドの変更をドキュメントに反映する TextEdit edit = compilationUnit.rewrite(document, null); edit.apply(document); // import文の変更をドキュメントに反映する TextEdit importEdit = importRewrite.rewriteImports(null); importEdit.apply(document);
CodeStyleConfigurationを経由してImportRewriteを生成する。
CodeStyleConfigurationを使うと、import文の並び順を勘案してくれる。
ImportRewrite#addImport()で、import文を追加しつつクラス名(FQCN)を単純名に変換する。
単純名が重複している(import文が追加できない)場合はFQCNのまま返ってくる。
ImportRewrite#rewriteImports()でTextEdit(変更箇所のオブジェクト)を取得できる。
これを使ってimport文の変更をドキュメントに反映する。
ASTNodeには、プレースホルダーという特殊なクラスが用意されている。[2014-05-10]
これを使うと、構文オブジェクトを自由な文字列で生成することが出来る。
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
ASTRewrite rewriter = ASTRewrite.create(ast);
Statement statement = (Statement) rewriter.createStringPlaceholder("//mycomment", ASTNode.EMPTY_STATEMENT);
Statement statement = (Statement) rewriter.createStringPlaceholder("Object obj = new Object();", ASTNode.VARIABLE_DECLARATION_STATEMENT);
NullLiteral literal = (NullLiteral) rewriter.createStringPlaceholder("/*TODO*/null", ASTNode.NULL_LITERAL);
ただし、プレースホルダーで作ったASTNodeでは、TextEdit#apply()を使ってソースを生成する際に、UNDOは別物として作られるようだ。
つまり、Javaエディター上でCtrl+Zを押すと、プレースホルダーで作った分だけが元に戻る。変更を全て戻そうとすると、何度もCtrl+Zを押さなくてはならない。
(プレースホルダーを使っていない場合、Ctrl+Zを押すと、変更したソースが全てまとめて元に戻る)