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


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というものがあるので、それをベースに作成した。
その方法はこちらが詳しい。

  • 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なしのプラグインとして、コードを実行できるようになる。
上で挙げたプログラムは、動けば何でも良かったので、プラグインとしての体裁は全く考えなかった。