JavaParser とは
- その名の通り、 Java のソースコードを構文解析するライブラリ
- ソースの解析結果を抽象構文木(AST)として取得でき、 Visitor パターンなどの方法で解析結果にアクセスできる
- Java のソース解析のライブラリといえば Eclipse JDT を使う方法がある1
- JDT は Eclipse プラグインのために使うのが本来の目的なのに対して、 JavaParser は純粋に Java の構文解析が目的となっている
- 必要なライブラリも jar 1つ(800KB ほど)なので、導入は楽っぽい
- Java 9 に対応しているらしい
- API に
Optional
が使用されているので、少なくとも Java 8 以上でしか使えない
Hello World
依存関係
dependencies {
compile 'com.github.javaparser:javaparser-core:3.3.0'
}
実装
package sample.javaparser;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) {
Path source = Paths.get("src/main/java/sample/javaparser/Main.java");
try {
CompilationUnit unit = JavaParser.parse(source);
System.out.println("***********************************************");
System.out.println(unit);
System.out.println("***********************************************");
} catch (IOException e) {
e.printStackTrace(System.err);
}
}
}
実行結果
***********************************************
package sample.javaparser;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) {
Path source = Paths.get("src/main/java/sample/javaparser/Main.java");
try {
CompilationUnit unit = JavaParser.parse(source);
System.out.println("***********************************************");
System.out.println(unit);
System.out.println("***********************************************");
} catch (IOException e) {
e.printStackTrace(System.err);
}
}
}
***********************************************
説明
-
JavaParser
のparse()
メソッドで Java ソースコードの解析を実行できる - 解析結果は
CompilationUnit
という型で返る-
CompilationUnit
はNode
クラスを継承している
-
VoidVisitor
実装
package sample.javaparser;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
public class MyVoidVisitor extends VoidVisitorAdapter<String> {
@Override
public void visit(VariableDeclarator n, String arg) {
System.out.println("n > " + n);
System.out.println("arg > " + arg);
super.visit(n, arg);
}
}
package sample.javaparser;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) {
Path source = Paths.get("src/main/java/sample/javaparser/Main.java");
try {
CompilationUnit unit = JavaParser.parse(source);
unit.accept(new MyVoidVisitor(), "ARG!!");
} catch (IOException e) {
e.printStackTrace(System.err);
}
}
}
実行結果
n > source = Paths.get("src/main/java/sample/javaparser/Main.java")
arg > ARG!!
n > unit = JavaParser.parse(source)
arg > ARG!!
説明
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
public class MyVoidVisitor extends VoidVisitorAdapter<String> {
@Override
public void visit(VariableDeclarator n, String arg) {
System.out.println("n > " + n);
System.out.println("arg > " + arg);
super.visit(n, arg);
}
- 構文木を走査していくための
Visitor
としてVoidVisitor
インターフェースが用意されている -
VoidVisitor
には、ノードの種類ごとにコールバックされるメソッドが100個弱宣言されている - これらを全て実装し、単純に再帰的にノードを走査していく実装として
VoidVisitorAdapter
クラスが用意されている - これを継承して独自の
Visitor
を作成する - 処理したいノードを受け取るメソッドを必要に応じてオーバーライドして、処理を実行する
- 最後に
super.visit()
を呼ぶことで、さらに子供のノードに対して走査が続けられるようになる
CompilationUnit unit = JavaParser.parse(source);
unit.accept(new MyVoidVisitor(), "ARG!!");
-
Visitor
は、CompliationUnit
クラスのaccept()
に渡すことで使用できる-
accept()
メソッドはVisitable
インターフェースで定義されており、CompliationUnit
はそれを実装している - ちなみに、
VoidVisitor
のvisit()
メソッドに走査したいノードを渡す方法もあるが、accept()
を使った場合も裏では結局このvisit()
メソッドが呼ばれていて動きは同じになる
-
@Override
public <A> void accept(VoidVisitor<A> v, A arg) {
v.visit(this, arg); // ★VoidVisitor の visit() をそのまま呼んでいる
}
-
accept()
(およびvisit()
)メソッドの第二引数に値を渡すと、その値はノードを走査しているあいだずっと連携され続ける- たぶん、共通で参照したいデータや処理の委譲先オブジェクトの連携用?
GenericVisitor
実装
package sample.javaparser;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.visitor.GenericVisitorAdapter;
public class MyGenericVisitor extends GenericVisitorAdapter<Integer, String> {
@Override
public Integer visit(VariableDeclarator n, String arg) {
System.out.println(arg);
System.out.println(n);
super.visit(n, arg);
return -1;
}
}
package sample.javaparser;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) {
Path source = Paths.get("src/main/java/sample/javaparser/Main.java");
try {
CompilationUnit unit = JavaParser.parse(source);
int result = unit.accept(new MyGenericVisitor(), "ARG!!");
System.out.println(result);
} catch (IOException e) {
e.printStackTrace(System.err);
}
}
}
実行結果
ARG!!
source = Paths.get("src/main/java/sample/javaparser/Main.java")
-1
説明
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.visitor.GenericVisitorAdapter;
public class MyGenericVisitor extends GenericVisitorAdapter<Integer, String> {
@Override
public Integer visit(VariableDeclarator n, String arg) {
System.out.println(arg);
System.out.println(n);
super.visit(n, arg);
return -1;
}
-
VoidVisitor
のvisit()
には戻り値がなかったが、GenericVisitor
のvisit()
には戻り値がある -
GenericVisitor
を実装した抽象クラスであるGenericVisitorAdaptor
は、走査中にvisit()
メソッドがnull
以外の値を返した場合、そこで走査を中断し、呼び出し元に返された値を戻す -
visit()
の第二引数は、VoidVisitor
と同じ扱いになる
ノードの種類
抽象構文木に含まれるノードは Node
クラスを継承しており、次のような継承関係になっている。
Node
┣━ArrayCreationLevel
┣━CatchClause
┣━CompilationUnit
┣━ImportDeclaration
┣━MemberValuePair
┣━ModuleDeclaration
┣━Name
┣━PackageDeclaration
┣━Parameter
┣━SimpleName
┣━VariableDeclarator
┃
┣━BodyDeclaration
┃ ┣━AnnotationMemberDeclaration
┃ ┣━EnumConstantDeclaration
┃ ┣━FieldDeclaration
┃ ┣━InitializerDeclaration
┃ ┣━CallableDeclaration
┃ ┃ ┣━ConstructorDeclaration
┃ ┃ ┗━MethodDeclaration
┃ ┗━TypeDeclaration
┃ ┣━AnnotationDeclaration
┃ ┣━ClassOrInterfaceDeclaration
┃ ┗━EnumDeclaration
┃
┣━Comment
┃ ┣━BlockComment
┃ ┣━JavadocComment
┃ ┗━LineComment
┃
┣━Expression
┃ ┣━ArrayAccessExpr
┃ ┣━ArrayCreationExpr
┃ ┣━ArrayInitializerExpr
┃ ┣━AssignExpr
┃ ┣━BinaryExpr
┃ ┣━CastExpr
┃ ┣━ClassExpr
┃ ┣━ConditionalExpr
┃ ┣━EnclosedExpr
┃ ┣━FieldAccessExpr
┃ ┣━InstanceOfExpr
┃ ┣━LambdaExpr
┃ ┣━MethodCallExpr
┃ ┣━MethodReferenceExpr
┃ ┣━NameExpr
┃ ┣━ObjectCreationExpr
┃ ┣━SuperExpr
┃ ┣━ThisExpr
┃ ┣━TypeExpr
┃ ┣━UnaryExpr
┃ ┣━VariableDeclarationExpr
┃ ┣━AnnotationExpr
┃ ┃ ┣━MarkerAnnotationExpr
┃ ┃ ┣━NormalAnnotationExpr
┃ ┃ ┗━SingleMemberAnnotationExpr
┃ ┗━LiteralExpr
┃ ┣━BooleanLiteralExpr
┃ ┣━NullLiteralExpr
┃ ┗━LiteralStringValueExpr
┃ ┣━CharLiteralExpr
┃ ┣━DoubleLiteralExpr
┃ ┣━IntegerLiteralExpr
┃ ┣━LongLiteralExpr
┃ ┗━StringLiteralExpr
┃
┣━ModuleStmt
┃ ┣━ModuleExportsStmt
┃ ┣━ModuleOpensStmt
┃ ┣━ModuleProvidesStmt
┃ ┣━ModuleRequiresStmt
┃ ┗━ModuleUsesStmt
┃
┣━Statement
┃ ┣━AssertStmt
┃ ┣━BlockStmt
┃ ┣━BreakStmt
┃ ┣━ContinueStmt
┃ ┣━DoStmt
┃ ┣━EmptyStmt
┃ ┣━ExplicitConstructorInvocationStmt
┃ ┣━ExpressionStmt
┃ ┣━ForeachStmt
┃ ┣━ForStmt
┃ ┣━IfStmt
┃ ┣━LabeledStmt
┃ ┣━LocalClassDeclarationStmt
┃ ┣━ReturnStmt
┃ ┣━SwitchEntryStmt
┃ ┣━SwitchStmt
┃ ┣━SynchronizedStmt
┃ ┣━ThrowStmt
┃ ┣━TryStmt
┃ ┣━UnparsableStmt
┃ ┗━WhileStmt
┃
┗━Type
┣━IntersectionType
┣━PrimitiveType
┣━UnionType
┣━UnknownType
┣━VoidType
┣━WildcardType
┗━ReferenceType
┣━ArrayType
┣━ClassOrInterfaceType
┗━TypeParameter
visit()
メソッドの引数で受け取れるのは、これらのうち末端のクラスになる(Type
や ReferenceType
など階層の途中のクラスを受け取るメソッドは無い)。
だいたいクラス名からコードのどの要素に対応しているかは想像できるが、一部想像できないものがあるので、その辺だけピックアップして動作を確認する。
ArrayCreationLevel
- 配列を宣言したときの初期サイズを指定しているところ
-
int[] array = new int[10];
の[10]
の部分
MemberValuePair
- アノテーションの引数を名前つきで指定している部分
-
@SomeAnnotation(value="text")
のvalue="text"
の部分
AnnotationMemberDeclaration
@interface MyAnnotation {
String value() default "text"; // ココと
int number(); // ココのこと
}
ConditionalExpr
- 三項演算子
EnclosedExpr
- 式の中で使われている
()
の括り
ExplicitConstructorInvocationStmt
- コンストラクタの中で明示的に呼び出されている
this()
やsuper()
UnionType
-
catch
句で複数の例外の型をcatch
している部分 -
} catch (OneException | OtherException e) {
ってしているところ
UnknownType
- ラムダ式の引数で型宣言が省略されたパラメータを表す Null オブジェクト
-
.forEach(line -> ...)
のline
の型
MarkerAnnotationExpr, NormalAnnotationExpr, SingleMemberAnnotationExpr
-
MarkerAnnotationExpr
-
@Override
のように、属性を一切使用しない使われ方をしている箇所
-
-
NormalAnnotationExpr
-
@SuppressWarnings(value="unused")
のように、属性名を指定して属性値を設定している箇所
-
-
SingleMemberAnnotationExpr
-
@SuppressWarnings("unused")
のように、単一のvalue
に対して属性名を省略して指定している箇所
-
調べたけどよくわからなかったやつら
Name
SimpleName
UnparsableStmt
IntersectionType
コメントの取得
行コメントやブロックコメントは、他の普通の Java 構文とは異なるルールで扱われる。
package sample.javaparser;
// a comment
// b comment
public class ParseCommentSample {
// c comment
private String name; // d comment
}
解析対象のクラス。
このクラスをパースしてコメントを取得してみる。
package sample.javaparser;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.comments.LineComment;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) {
Path source = Paths.get("src/main/java/sample/javaparser/ParseCommentSample.java");
try {
CompilationUnit unit = JavaParser.parse(source);
unit.accept(new VoidVisitorAdapter<Void>() {
@Override
public void visit(LineComment n, Void arg) {
System.out.println(n);
super.visit(n, arg);
}
}, null);
} catch (IOException e) {
e.printStackTrace(System.err);
}
}
}
実行結果
// d comment
// b comment
// a comment
と // c comment
は無視された。
ノードに付属したコメント と 孤立したコメント
コメントは、以下のいずれかに分類される。
- ノードに付属したコメント
- 孤立したコメント
VoidVisitor
を次のように実装して動作を確認してみる。
package sample.javaparser;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) {
Path source = Paths.get("src/main/java/sample/javaparser/ParseCommentSample.java");
try {
CompilationUnit unit = JavaParser.parse(source);
System.out.println("unit orphan comment > " + unit.getOrphanComments());
unit.accept(new VoidVisitorAdapter<Void>() {
@Override
public void visit(ClassOrInterfaceDeclaration n, Void arg) {
System.out.println("<<ClassOrInterfaceDeclaration>>");
System.out.println("comment > " + n.getComment());
System.out.println("orphan comment > " + n.getOrphanComments());
super.visit(n, arg);
}
@Override
public void visit(FieldDeclaration n, Void arg) {
System.out.println("<<FieldDeclaration>>");
System.out.println("comment > " + n.getComment());
super.visit(n, arg);
}
}, null);
} catch (IOException e) {
e.printStackTrace(System.err);
}
}
}
実行結果
unit orphan comment > [// a comment
]
<<ClassOrInterfaceDeclaration>>
comment > Optional[// b comment
]
orphan comment > [// c comment
]
<<FieldDeclaration>>
comment > Optional[// d comment
]
-
Node
にはgetComment()
とgetOrphanComment()
という、そのノードに紐づくコメントを取得するためのメソッドが用意されている - コメントが何らかのノードと隣接している場合、コメントはそのノードに付属するものとして扱われる
- その場合、コメントは付属するノードから
getComment()
で取得できるようになる - 一方で、どのノードとも紐づいていないコメントは孤立したコメントとして扱われる
- 孤立したコメントは、
getOrphanComment()
で取得することができる -
LineComment
やBlockComment
を受け取るvisit()
メソッドが呼ばれるのは、どこかのノードに付属しているコメントだけになる - 孤立したコメントも処理したい場合は、
getOrphanComment()
を使うか、もしくはgetAllContainedComments()
を使えば全てのコメントが取得できる
ちなみに、1つのノードに紐づくコメントは1つだけなので(getComment()
は単一のコメントしか返さない)、
// a comment
String s = "xxx"; // b comment
のようになっている場合は // b comment
が変数宣言に紐づくコメントとして扱われ、 // a comment
は孤立コメントとなる(そういうルール)。
文字列として出力する
package sample.javaparser;
/**
* Javadoc
*/
public class ToStringSample {
// line comment
/**
* Javadoc
* @param age age
* @param name name
*/
public void method(
int age,
String name
) {
/* block comment */
System.out.println(
"age=" + age +
"name=" + name
);
}
}
わざと改行を入れてフォーマットに特徴をつけている。
package sample.javaparser;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) {
Path source = Paths.get("src/main/java/sample/javaparser/ToStringSample.java");
try {
CompilationUnit unit = JavaParser.parse(source);
System.out.println(unit);
} catch (IOException e) {
e.printStackTrace(System.err);
}
}
}
実行結果
package sample.javaparser;
/**
* Javadoc
*/
public class ToStringSample {
// line comment
/**
* Javadoc
* @param age age
* @param name name
*/
public void method(int age, String name) {
/* block comment */
System.out.println("age=" + age + "name=" + name);
}
}
説明
- 普通にノードを
toString()
すると、入力のままの形ではなく、ある程度整形された文字列になる - また、コメントは消されることなく出力される
整形の方法を調整する
package sample.javaparser;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.printer.PrettyPrinterConfiguration;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) {
Path source = Paths.get("src/main/java/sample/javaparser/ToStringSample.java");
try {
CompilationUnit unit = JavaParser.parse(source);
PrettyPrinterConfiguration conf = new PrettyPrinterConfiguration();
conf.setIndent(" ");
conf.setPrintComments(false);
conf.setPrintJavaDoc(false);
conf.setEndOfLineCharacter("<改行>\n");
System.out.println(unit.toString(conf));
} catch (IOException e) {
e.printStackTrace(System.err);
}
}
}
実行結果
package sample.javaparser;<改行>
<改行>
public class ToStringSample {<改行>
<改行>
public void method(int age, String name) {<改行>
System.out.println("age=" + age + "name=" + name);<改行>
}<改行>
}<改行>
説明
-
toString()
の引数にPrettyPrinterConfiguration
を渡すことで、構文木を文字列に戻すときの整形方法を指定できる。
メソッド | 説明 | デフォルト |
---|---|---|
setIndent(String) |
インデント1つに使用する文字列を指定する | (スペース4つ) |
setPrintComments(boolean) |
コメントを表示するか指定する | true |
setPrintJavaDoc(boolean) |
Javadoc を表示するか指定する | true |
setEndOfLineCharacter(String) |
改行で使用する文字列を指定する | System.getProperty("line.separator") |
構文木を書き換える
package sample.javaparser;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.stmt.BlockStmt;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) {
Path source = Paths.get("src/main/java/sample/javaparser/Main.java");
try {
CompilationUnit unit = JavaParser.parse(source);
unit.getClassByName("Main").ifPresent(mainClass -> {
// private final String textValue; フィールドを追加
mainClass.addField(String.class, "textValue", Modifier.PRIVATE, Modifier.FINAL);
// textValue を初期化するようにコンストラクタを追加
ConstructorDeclaration constructor = mainClass.addConstructor(Modifier.PUBLIC);
constructor.addParameter(String.class, "textValue"); // コンストラクタの引数追加
BlockStmt body = constructor.createBody(); // コンストラクタのボディを追加
body.addStatement("this.textValue = textValue;");
});
System.out.println(unit);
} catch (IOException e) {
e.printStackTrace(System.err);
}
}
}
実行結果
package sample.javaparser;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.stmt.BlockStmt;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args) {
Path source = Paths.get("src/main/java/sample/javaparser/Main.java");
try {
CompilationUnit unit = JavaParser.parse(source);
unit.getClassByName("Main").ifPresent(mainClass -> {
// private final String textValue; フィールドを追加
mainClass.addField(String.class, "textValue", Modifier.PRIVATE, Modifier.FINAL);
// textValue を初期化するようにコンストラクタを追加
ConstructorDeclaration constructor = mainClass.addConstructor(Modifier.PUBLIC);
// コンストラクタの引数追加
constructor.addParameter(String.class, "textValue");
// コンストラクタのボディを追加
BlockStmt body = constructor.createBody();
body.addStatement("this.textValue = textValue;");
});
System.out.println(unit);
} catch (IOException e) {
e.printStackTrace(System.err);
}
}
private final String textValue;
public Main(String textValue) {
this.textValue = textValue;
}
}
-
Node
には構文木を任意の形に書き換えるためのメソッドが用意されている - IDE のメソッド補完を見ながらやったらだいたいわかる程度には分かりやすい API になっている
-
remove()
など削除メソッドもある