Posted at

EclipseJDTのASTParserでJavaソースコードの抽象構文木とかHeadlessなEclipseプラグインとか

More than 1 year has passed since last update.

WARNING: この記事の内容は、2014/02に記述したメモです。内容が古くなっていますので、ご注意ください。

あと、間違っている記述があるかもしれません。。。


概要

Eclipseの内部で使われているJavaのASTパーサー (org.eclipse.jdt.core.dom.ASTParser) を使って、Javaのソースコードを解析してみた話。大学時代に弄ってたもの。

正直なところ、この記事の内容はこちらのページにのほうが詳しい。


何ができたか

つくったもの: headless

MySQLのデータベースに、こんなテーブル達を作成して、ASTから取得できる情報をぶち込む代物です。

mysql> show tables;

+--------------------+
| Tables_in_headless |
+--------------------+
| element |
| element_location |
| method |
| method_call_map |
| package |
| type |
| type_hierarchie |
+--------------------+
7 rows in set (0.02 sec)

こんなことして遊んだりできます。

(以下は、 hanzawa_java を処理してみた例です。)

mysql> select type_text, element_text from element where node_kind_text="Variable" limit 3\G

*************************** 1. row ***************************
type_text: ExprEditor
element_text: editor
*************************** 2. row ***************************
type_text: String
element_text: agentArgs
*************************** 3. row ***************************
type_text: Instrumentation
element_text: inst
3 rows in set (0.00 sec)

ファイル内での各言語要素の出現位置も記録します。

mysql> select * from element_location limit 3\G

*************************** 1. row ***************************
map_id: 1
pakage_id: 3
type_id: 1
method_id: NULL
element_id: 1
file: file:/Users/hiraro/Dropbox/workspace/headless_workspace/hanzawa_java/src/net/umanohone/Hanzawa.java
offset: 417
length: 7
line_start: 15
line_end: 15
last_update: 2016-12-04 21:00:03
*************************** 2. row ***************************
map_id: 2
pakage_id: 3
type_id: 1
method_id: NULL
element_id: 2
file: file:/Users/hiraro/Dropbox/workspace/headless_workspace/hanzawa_java/src/net/umanohone/Hanzawa.java
offset: 436
length: 20
line_start: 15
line_end: 15
last_update: 2016-12-04 21:00:03
*************************** 3. row ***************************
map_id: 3
pakage_id: 3
type_id: 1
method_id: NULL
element_id: 3
file: file:/Users/hiraro/Dropbox/workspace/headless_workspace/hanzawa_java/src/net/umanohone/Hanzawa.java
offset: 828
length: 10
line_start: 28
line_end: 28
last_update: 2016-12-04 21:00:03
3 rows in set (0.01 sec)

メソッド呼び出し関係など。

mysql> select method_id, name, signature, length from method order by length DESC limit 3\G

*************************** 1. row ***************************
method_id: 3
name: transform
signature: protected byte[] transform(byte[])
length: 337
*************************** 2. row ***************************
method_id: 2
name: transform
signature: public byte[] transform(java.lang.ClassLoader, java.lang.String, Class<?>, java.security.ProtectionDomain, byte[]) throws java.lang.instrument.IllegalClassFormatException
length: 244
*************************** 3. row ***************************
method_id: 1
name: premain
signature: public static void premain(java.lang.String, java.lang.instrument.Instrumentation)
length: 109
3 rows in set (0.00 sec)

mysql> select * from method_call_map limit 3\G
*************************** 1. row ***************************
map_id: 1
text: inst.addTransformer(new Hanzawa())
caller_id: 1
callee_id: 4
last_update: 2016-12-04 21:00:03
*************************** 2. row ***************************
map_id: 2
text: transform(classfileBuffer)
caller_id: 2
callee_id: 3
last_update: 2016-12-04 21:00:04
*************************** 3. row ***************************
map_id: 3
text: ClassPool.getDefault()
caller_id: 3
callee_id: 5
last_update: 2016-12-04 21:00:05
3 rows in set (0.00 sec)


実装について


必要なライブラリなど

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

Mavenで依存性解決したらこんな感じだった。

<dependencies>

<dependency>
<groupId>org.eclipse.core</groupId>
<artifactId>contenttype</artifactId>
<version>3.4.200-v20130326-1255</version>
</dependency>
<dependency>
<groupId>org.eclipse.core</groupId>
<artifactId>jobs</artifactId>
<version>3.5.300-v20130429-1813</version>
</dependency>
<dependency>
<groupId>org.eclipse.core</groupId>
<artifactId>org.eclipse.core.resources</artifactId>
<version>3.7.100</version>
</dependency>
<dependency>
<groupId>org.eclipse.core</groupId>
<artifactId>org.eclipse.core.runtime</artifactId>
<version>3.7.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.equinox</groupId>
<artifactId>org.eclipse.equinox.app</artifactId>
<version>1.3.100</version>
</dependency>
<dependency>
<groupId>org.eclipse.equinox</groupId>
<artifactId>common</artifactId>
<version>3.6.200-v20130402-1505</version>
</dependency>
<dependency>
<groupId>org.eclipse.equinox</groupId>
<artifactId>preferences</artifactId>
<version>3.5.100-v20130422-1538</version>
</dependency>
<dependency>
<groupId>org.eclipse.equinox</groupId>
<artifactId>preferences</artifactId>
<version>3.5.100-v20130422-1538</version>
</dependency>
<dependency>
<groupId>org.eclipse.osgi</groupId>
<artifactId>org.eclipse.osgi</artifactId>
<version>3.7.1</version>
</dependency>
</dependencies>


ASTの構築・解析(ソースコードを文字列でぶっこむ方式)

ASTParser parser = ASTParser.newParser(AST.JLS4);

parser.setSource(srcStr.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
CompilationUnit unit = (CompilationUnit) parser.createAST(new NullProgressMonitor());
unit.accept(new ASTVisitor(){
@Override
public boolean visit(MethodDeclaration node) {
/* do something about MethodDeclaration node */
return super.visit(node);
}

});

ASTParserのインスタンスを作って、解析したいソースコード文字列をchar配列としてぶっこむ。

ちなみに、parser.setKindのところの引数には、K_COMPILATION_UNITの他に、K_EXPRESSIONとかK_STATEMENTSなども渡すことができる(詳細不明)。

構築したASTは、Visitorパターンで順次訪問していける。ASTVisitorクラスを継承して、独自のvisitメソッドなどをオーバーライドすればよい。


ASTの構築・解析(Eclipseのプロジェクトを渡して詳しく解析する方式)

変数の参照とかメソッド呼び出しなどから、それらの型情報とか定義の取得などを考えようとすると、プログラムのソースファイル全体をASTParserに渡してあげる必要性がでてくる。

そのための、

ASTParser.createASTs(String[] sourceFilePaths, String[] encodings, String[] bindingKeys,

FileASTRequestor requestor, IProgressMonitor monitor)

というメソッドもある。

しかし、引数の指定が面倒くさかったり、なぜかうまく動かせなかったので、

ASTParser.setProject(IJavaProject project)

というメソッドで、EclipseのJavaプロジェクトに相当するオブジェクトIJavaProjectを渡した。


IJavaProjectの生成

IJavaProject project =JavaCore.create(ResourcesPlugin.getWorkspace().

getRoot().getProject(projectName));

ここで、ResourcesPlugin.getWorkspace() は、Eclipseのプラグイン内でしか呼び出すことができないらしい。

そこで、アプリケーション全体をEclipseのプラグインとして実装する必要が生じる。


HeadlessなEclipseプラグイン作成

UIなしの超単純なプラグインのテンプレートとして、HeadlessHelloRCPというものがあるので、それをベースに作成した。

その方法はこちらが詳しい。

20140221170911.png

20140221170915.png



  • Plugin-in Project を選んでプロジェクト作成のウィザード開く


  • Contentページで


    • チェック外す


      • Generate an acivator, a Java class that ...

      • This plug-in will make contributions to the UI




    • Would you like ... rich client application?Yes にする




  • Templates ページで Headless Hello RCP を選ぶ

これでとりあえず、GUIなしのプラグインとして、コードを実行できるようになる。

上で挙げたプログラムは、動けば何でも良かったので、プラグインとしての体裁は全く考えなかった。