S-JIS[2013-11-18/2014-11-27] 変更履歴

Eclipse JDT AST

Eclipseプラグイン開発JDTのAST(abstract syntax tree・抽象構文木)について。


概要

JDTのASTは、DOM(Document Object Model)と同じ方式のJavaソースの構文解析木。

IType等のクラスではJavaソースを変更することは出来ないが、ASTでは変更することが出来る。

ASTではASTNodeというクラスのツリーという形でソースの解析結果が保持される。


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の走査

ASTNode内の各要素(フィールドやメソッド、メソッドの中の文やimport文なども!)をVisitorパターンで走査することが出来る。[2013-11-27]

メソッド名でメソッド定義のASTNode(MethodDeclaration)を検索する例。
(MethodDeclarationはASTNodeの具象クラス。メソッド定義を表す)

呼び出す側.java:

		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());
		}

MethodFindVisitor.java:

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の走査

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の取得

クラスを表す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の加工

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の作成

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は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文

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


JDTへ戻る / Eclipseプラグインへ戻る / Eclipseへ戻る / 技術メモへ戻る
メールの送信先:ひしだま