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を押すと、変更したソースが全てまとめて元に戻る)