読者です 読者をやめる 読者になる 読者になる

イマカラブログ

イマカラメガネの中の人が、芝居と関係あることないこと好き勝手に書くブログ。

スタンドアロンでJDTのASTを使う

f:id:imakaramegane:20170109201859j:plain

必要なライブラリ

JDTのASTをスタンドアロンで使いたい場合は、 Eclipseプラグインが格納されているフォルダにある、 以下のjarを拾ってきてクラスパスを通せば使える (アスタリスク(*)の部分は、実際にはバージョン番号とかになってる)。

Mavenのセントラルリポジトリにも登録されてるので、そこから拾うのが簡単かな。

pom.xmlはこんな感じ。

  <dependencies>
    <dependency>
      <groupId>org.eclipse.core</groupId>
      <artifactId>org.eclipse.core.contenttype</artifactId>
      <version>3.4.100.v20100505-1235</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.core</groupId>
      <artifactId>org.eclipse.core.jobs</artifactId>
      <version>3.5.100</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.birt.runtime</groupId>
      <artifactId>org.eclipse.core.resources</artifactId>
      <version>3.10.0.v20150423-0755</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.common</artifactId>
      <version>3.6.0.v20100503</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.equinox</groupId>
      <artifactId>org.eclipse.equinox.preferences</artifactId>
      <version>3.4.1</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jdt</groupId>
      <artifactId>org.eclipse.jdt.core</artifactId>
      <version>3.10.0</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse</groupId>
      <artifactId>org.eclipse.osgi</artifactId>
      <version>3.8.0.v20120529-1548</version>
    </dependency>
  </dependencies>

基本的な使い方

ASTParser#createASTを使ってASTを作成。 返却されたASTNodeacceptメソッドにASTVisitorを継承した自作クラスの インスタンスを引き渡してツリーを走査する(Visitorパターン)。

    String source = ...;  // 解析するJavaソースの文字列
    ASTParser parser = ASTParser.newParser( AST.JLS8 );
    Map<?, ?> options = JavaCore.getOptions();
    JavaCore.setComplianceOptions( JavaCore.VERSION_1_8, options );
    parser.setCompilerOptions( options );
    parser.setSource( source.toCharArray() );
    ASTNode ast = parser.createAST( null );
    ast.accept( new ASTVisitor() { ... } );

以下、ソースの断片を説明。

ASTParser#newParser

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

ASTParserインスタンスを作成するときにJavaAPIレベルを指定しないといけないんだけど、 AST.JLS8が全バージョンに対応してるので、 固定でAST.JLS8を指定すればOK。 というか他が全部deprecatedになってるので選択の余地はない。

ASTParser#setCompilerOptions

    Map<?, ?> options = JavaCore.getOptions();
    JavaCore.setComplianceOptions( JavaCore.VERSION_1_8, options );
    parser.setCompilerOptions( options );

解析時に使用されるコンパイラオプションを引数に指定している。 ASTParser#setSource(char[])を使って1.5以降のソースを解析する場合、 このメソッドを使って適切なコンパイラオプションを指定する必要がある。

注意点として、ASTParser#setSourceIClassFileおよびICompilationUnitを引き渡した場合、 設定したコンパイラオプションがリセットされるらしい。

あと、optionsの中身を見るとソースのフォーマットに関するオプションも入ってた。 ASTでソースを改変することがあったら、細かく調べてみようかな。

ASTParser#createAST

    parser.setSource( source.toCharArray() );
    ASTNode ast = parser.createAST( null );

ASTParser#setSourceで設定したソースを解析してASTを構築して返却してくれる。 あとは、返却されたASTNodeインスタンスから各ノードを辿って自分の欲しい情報を 取得する。

返却されたASTNodeはデフォルトだとCompilationUnitインスタンスみたい。 ASTParser#setKindで変えることができるようだけど、必要になったときに調べる。

引数にIProgressMonitorインスタンスを渡せば、解析の進捗状況を確認したり、 途中でキャンセルできるみたい。必要ない場合はnullでOK。

おまけ

ソースファイルの中身を丸っと取得するコード。

    String source = new String(
            Files.readAllBytes( Paths.get( "Hoge.java" ) ), "utf-8" );

参考

blog.cles.jp

(2017/1/10 追記) もっと詳しい情報があった。。。 qiita.com

ITypeBindingから取得できる名前

JDTのASTを弄るときに、ITypeBindingから取得できる名前がどの形式で取得できたか毎度忘れちゃうのでメモ。

ITypeBinding#getQualifiedName

わかってるつもりだったんだけど、時々、思ったような結果が得られなくて混乱したので、Javadocを見ながら挙動を確認する。

Returns the fully qualified name of the type represented by this binding if it has one.

このバインディングが持つ型の完全修飾名を返します。

  • For top-level types, the fully qualified name is the simple name of the type preceded by the package name (or unqualified if in a default package) and a ".". Example: "java.lang.String" or "java.util.Collection". Note that the type parameters of a generic type are not included.

    最上位タイプの場合、完全修飾名は、パッケージ名の前にあるタイプの単純名です(デフォルトパッケージの場合は修飾されません)。例: "java.lang.String"または "java.util.Collection"。ジェネリック型の型パラメータは含まれていないことに注意してください。

TypeDeclarationでの取得例(行コメントの右側)。参考までにITypeBinding#getNameの取得結果もあわせて記載(行コメントの左側)。

package sample;
class TopLevelClass1 {} // -> "TopLevelClass1", "sample.TopLevelClass1"
class TopLevelClass2<T> {} // -> "TopLevelClass2", "sample.TopLevelClass2"
  • For members of top-level types, the fully qualified name is the simple name of the type preceded by the fully qualified name of the enclosing type (as computed by this method) and a ".". Example: "java.io.ObjectInputStream.GetField". If the binding is for a member type that corresponds to a particular instance of a generic type arising from a parameterized type reference, the simple name of the type is followed by the fully qualified names of the type arguments (as computed by this method) surrounded by "<>" and separated by ",". Example: "pkg.Outer.Inner<java.lang.String>".

    トップレベル型のメンバの場合、完全修飾名は、(このメソッドで計算された)囲み型の完全修飾名と "。"で始まる型の単純名です。例: "java.io.ObjectInputStream.GetField"。パラメータ化された型参照から生じるジェネリック型の特定のインスタンスに対応するメンバ型のバインディングの場合、その型の単純名の後に(このメソッドによって計算される)型引数の完全修飾名が囲まれます。 "<>"で区切られ、 "、"で区切られます。例: "pkg.Outer.Inner <java.lang.String>"。

TypeDeclarationでの取得例。ネストしたクラスおよびインナークラスにおいてもジェネリクスのパラメタ型が含まれていないことが確認できる。

package sample;
class TopLevelClass {
    static class NestedClass1 {} // -> "NestedClass1", "sample.TopLevelClass.NestedClass1"
    static class NestedClass2<T> {} // -> "NestedClass2", "sample.TopLevelClass.NestedClass2"
    static class NestedClass3<T extends Object> {} // -> "NestedClass3", "sample.TopLevelClass.NestedClass3"
    class InnerClass1{} // -> "InnerClass1", "sample.TopLevelClass.InnerClass1"
    class InnerClass2<T>{} // -> "InnerClass2", "sample.TopLevelClass.InnerClass2"
    class InnerClass3<T extends Object>{} // -> "InnerClass3", "sample.TopLevelClass.InnerClass3"
}

FieldDeclarationおよびMethodDeclaration#getReturnType2での取得例。ジェネリクスの型引数も完全修飾名が付いた状態で取得できるのが確認できる。

package sample;
class TopLevelClass {
    String field1; // -> "String", "java.lang.String"
    List field2; // -> "List", "java.util.List"
    List<?> field3; // -> "List<?>", "java.util.List<?>"
    List<String> field4; // -> "List<String>", "java.util.List<java.lang.String>"
    
    String method1() { return null; } // -> "String", "java.lang.String"
    List method2() { return null; } // -> "List", "java.util.List"
    List<?> method3() { return null; } // -> "List<?>", "java.util.List<?>"
    List<String> method4() { return null; } // -> "List<String>", "java.util.List<java.lang.String>"
}
  • For primitive types, the fully qualified name is the keyword for the primitive type. Example: "int".

    プリミティブ型の場合、完全修飾名はプリミティブ型のキーワードです。例: "int"

  • For the null type, the fully qualified name is the string "null".

    null型の場合、完全修飾名は文字列 "null"です。

書いてあるとおり。わかりやすい。

  • Local types (including anonymous classes) and members of local types do not have a fully qualified name. For these types, and array types thereof, this method returns an empty string.

    ローカル・タイプ(匿名クラスを含む)およびローカル・タイプのメンバーには、完全修飾名はありません。これらの型およびその配列型の場合、このメソッドは空の文字列を返します。

う~ん?配列になってるfield2var2の名前に一貫性なくない?var2が"[]"を返却してるのって、Javadocの説明とあってないのでは?

package sample;
class TopLevelClass {
    void method() {
        class LocalClass {  // -> "LocalClass", ""
            LocalLocalClass field1;  // -> "LocalLocalClass", ""
            LocalLocalClass[] field2; // -> "LocalLocalClass[]", "[]"(?)
            class LocalLocalClass {} // -> "LocalLocalClass", ""
        }
        LocalClass var1;  // -> "LocalClass", ""
        LocalClass[] var2;  // -> ""(?), ""
        new TopLevelClass() {}; // -> "", ""
    }
}
  • For array types whose component type has a fully qualified name, the fully qualified name is the fully qualified name of the component type (as computed by this method) followed by "". Example: "java.lang.String".

    コンポーネント型が完全修飾名を持つ配列型の場合、完全修飾名は([このメソッドで計算される])コンポーネント型の完全修飾名で、その後に""が続きます。例: "java.lang.String "。

上で試したケースで足りてるかな。コンポーネントってなんのこと?

  • For type variables, the fully qualified name is just the name of the type variable (type bounds are not included). Example: "X".

    型変数の場合、完全修飾名は型変数の名前にすぎません(型境界は含まれません)。例: "X"

書いてあるとおり。わかりやすい。

  • For type bindings that correspond to particular instances of a generic type arising from a raw type reference, the fully qualified name is the fully qualified name of the erasure type. Example: "java.util.Collection". Note that the the type parameters are omitted.

    raw type参照から生じるジェネリック型の特定のインスタンスに対応する型バインディングの場合、完全修飾名は消去型の完全修飾名です。例: "java.util.Collection"。型パラメータは省略されていることに注意してください。

これも上で試したケースで足りてるかな。List var;だと"java.util.List"ってことよね?

  • For type bindings that correspond to particular instances of a generic type arising from a parameterized type reference, the fully qualified name is the fully qualified name of the erasure type followed by the fully qualified names of the type arguments surrounded by "<>" and separated by ",". Example: "java.util.Collection<java.lang.String>".

    パラメータ化された型参照から生じるジェネリック型の特定のインスタンスに対応する型バインディングの場合、完全修飾名は、イレージャ型の完全修飾名、続いて "<>"で囲まれた型引数の完全修飾名"、"によって。例: "java.util.Collection <java.lang.String>"。

これも上で試したケースで足りてる。List<String> var;だと"java.util.List<java.lang.String"

  • For wildcard types, the fully qualified name is "?" optionally followed by a single space followed by the keyword "extends" or "super" followed a single space followed by the fully qualified name of the bound (as computed by this method) when present. Example: "? extends java.io.InputStream".

    ワイルドカードタイプの場合、完全修飾名は "?"です。オプションで、単一のスペースの後にキーワード "extends"または "super"が続き、単一のスペースの後に(このメソッドで計算された)完全修飾名が続きます。例: "?extends java.io.InputStream"。

ジェネリクス関連をまとめてお試し。

class TopLevelClass {
    List var1;  // -> "List", "java.lang.List"
    List<Number> var2;  // -> "List<Number>", "java.util.List<java.lang.Number"
    List<?> var3;  // -> "List<?>", "java.util.List<?>"
    List<? extends Number> var4;  // -> "List<? extends Number>", "java.util.List<? extends java.lang.Number>"
    List<? super Number> var5;  // -> "List<? super Number>", "java.util.List<? super java.lang.Number>"
}
  • Capture types do not have a fully qualified name. For these types, and array types thereof, this method returns an empty string.

    キャプチャー・タイプには完全修飾名はありません。これらの型およびその配列型の場合、このメソッドは空の文字列を返します。

キャプチャ・タイプって、下の場合のaddの引数の型のことでいいのかな?確かに空文字列が返却される。

class TopLevelClass {
    void method(List<?> lst) {
        lst.add( null );  // -> "", ""
    }
}

ITypeBinding#getBinaryName

Returns the binary name of this type binding. The binary name of a class is defined in the Java Language Specification 3rd edition, section 13.1. Note that in some cases, the binary name may be unavailable. This may happen, for example, for a local type declared in unreachable code.

この型バインディングバイナリ名を返します。クラスのバイナリ名は、Java言語仕様第3版13.1節で定義されています。 場合によっては、バイナリ名が使用できないことがあります。これは、たとえば、到達不能なコードで宣言されたローカルタイプに対して発生します。

デバッガで見たときに確認できる名前。
基本的にITypeBinding#getQualifiedNameと同じ文字列になるので、異なる文字列になったものだけ下に掲載。

package sample;
class TopLevelClass {
    static class NestedClass1 {} // -> "sample.TopLevelClass$NestedClass1"
    static class NestedClass2<T> {} // -> "sample.TopLevelClass$NestedClass2"
    static class NestedClass3<T extends Object> {} // -> "sample.TopLevelClass$NestedClass3"
    class InnerClass1{} // -> "sample.TopLevelClass$InnerClass1"
    class InnerClass2<T>{} // -> "sample.TopLevelClass$InnerClass2"
    class InnerClass3<T extends Object>{} // -> "sample.TopLevelClass$InnerClass3"
}
class TopLevelClass {
    List<?> field3; // -> "java.util.List"
    List<String> field4; // -> "java.util.List"
    
    List<?> method3() { return null; } // -> "java.util.List"
    List<String> method4() { return null; } // -> "java.util.List"
}
class TopLevelClass {
    void method() {
        class LocalClass {  // -> "sample.TopLevelClass$1LocalClass"
            LocalLocalClass field1;  // -> "sample.TopLevelClass$1LocalClass$LocalLocalClass"
            LocalLocalClass[] field2; // -> "[Lsample.TopLevelClass$1LocalClass$LocalLocalClass"
            class LocalLocalClass {} // -> "sample.TopLevelClass$1LocalClass$LocalLocalClass"
        }
        LocalClass var1;  // -> "sample.TopLevelClass$1LocalClass"
        LocalClass[] var2;  // -> "[Lsample.TopLevelClass$1LocalClass"
        new TopLevelClass() {}; // -> "sample.TopLevelClass$1"
    }
}
class TopLevelClass {
    List var1;  // -> "java.lang.List"
    List<Number> var2;  // -> "java.util.List"
    List<?> var3;  // -> "java.util.List"
    List<? extends Number> var4;  // -> "java.util.List"
    List<? super Number> var5;  // -> "java.util.List"
}

違いとしては、

  • トップレベルのクラスから下の区切り文字が"."の代わりに"$"。
  • 配列の表現。
  • 型パラメータの情報がなくなる。

なんだけど、キャプチャ型の場合、nullが返却された。

class TopLevelClass {
    void method(List<?> lst) {
        lst.add( null );  // -> null
    }
}

一応、ITypeBinding#getErasureを使えば"java.lang.Object"は取得できた。

参考

ジェネリクスの代入互換のカラクリ - プログラマーの脳みそ

ちょっと思うところあって

別に新年だからというわけではないけれど、ちょっと思うところがあってブログを始めてみようと思う。
はてなも「はてなのブログ」始めたしね。

月に1記事ぐらいは書くように頑張ってみましょ。