卒業研究にJavaParserを使ったのでその紹介をする
はじめに
私の卒業研究では、Javaのソースコードからクラス図を生成するツールの開発を行ないました。
クラス図をコードから生成するツールは既にありそうですが、要件上クラス図が画像だと問題があったためクラス図を手作業でクソめんどいけど作ることになりました。
クラス図を生成するには、それに必要な情報をソースコードから抜き出さなければなりません。
そのためにJavaParserという構文解析ライブラリを使ったのですが、これを紹介している記事が少なかったので紹介しようと思い、この記事を執筆しました。
JavaParser
JavaParserはJavaのソースコードを解析するライブラリです。
JavaParserによって解析されたコードは、一旦抽象構文木に変換されます。
ユーザーはVisitorパターンを用いることで、抽象構文木中の各ノードにアクセスします。
実装
import com.github.javaparser.StaticJavaParser;
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 path = Paths.get("path/to/Example.java");
CompilationUnit unit;
try {
unit = StaticJavaParser.parse(path);
Visitor visitor = new Visitor();
unit.accept(visitor, null);
} catch (IOException e) {
e.printStackTrace();
}
}
}
StaticJavaParser.parse()
で解析を行ない、unit.accept()
で後述のVisitorクラスのvisit()
を開始します。
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
public class Visitor extends VoidVisitorAdapter<Void> {
@Override
public void visit(ClassOrInterfaceDeclaration c, Void arg) {
System.out.println("クラス名: " + c.getNameAsString());
System.out.println("\t範囲: " + c.getRange().orElseThrow());
System.out.println("\t継承: " + c.getExtendedTypes());
System.out.println();
super.visit(c, arg);
}
@Override
public void visit(MethodDeclaration m, Void arg) {
System.out.println("メソッド名: " + m.getNameAsString());
System.out.println("\t範囲: " + m.getRange().orElseThrow());
System.out.println("\t返り値の型: " + m.getType().asString());
System.out.println();
super.visit(m, arg);
}
@Override
public void visit(ConstructorDeclaration c, Void arg) {
System.out.println("コンストラクター名: " + c.getNameAsString());
System.out.println("\t範囲: " + c.getRange().orElseThrow());
System.out.println();
super.visit(c, arg);
}
@Override
public void visit(FieldDeclaration f, Void arg) {
for (VariableDeclarator v : f.getVariables()) {
System.out.println("バリアブル名: " + v.getNameAsString());
System.out.println("\t範囲: " + v.getRange().orElseThrow());
System.out.println("\t型: " + v.getType().asString());
System.out.println();
}
super.visit(f, arg);
}
}
Visitor
クラスでは、各ノードに対する処理を記述するためのvisit()
を実装するためVoidVisitorAdapter
クラスを継承します。
ジェネリックになっており、そこには引数として渡したい型を、何もいらないならVoid
を指定します。
各visit()
では、各ノードに対する処理を指定します。
第1引数にはクラスの宣言やらメソッドの宣言やらを表す型を指定するのですが、その一覧は以下のドキュメントから確認できます。
VoidVisitorAdapter (javaparser-core 3.3.1 API)
クラスの宣言部分があった時の処理を書くならClassOrInterfaceDeclaration
、メソッドならMethodDeclaration
など、適切な型を選べます。
第2引数にはVoidVisitorAdapter
の要素の型で指定します。
visit()
内ではそれぞれのノードに対する処理を記述します。
最後にはsuper.visit(hoge, fuga);
とすることで、再帰的にvisit()
が呼ばれて深さ優先的にノードを走査し、各visit()
の条件にあうノードに到達したらその処理を行なう、という感じになっています。
各ノードの情報には、各クラスのメンバーにアクセスすることで取得できます。
例えば.getRange()
では宣言の範囲を、.getNameAsString()
でその名前を取得できます。
FieldDeclaration
のvisit()
では、他のとは違ってfor
で回しています。
Javaでは、以下のようにフィールドを1行で複数個定義できます。
int a, b; // Field
// ^ ^ Variable
JavaParserでは1行で定義してあるのをField、個々をVariableとしており、今回は個別のVariableがほしいためfor
で回しています。
出力
そんなこんなで、次の入力ファイルの例
public class Example {
int foo;
String bar1, bar2;
public Example() {
}
public int GetFoo() {
return 0;
}
}
に対して、書いたコードを実行した時の出力は次のようになります。
クラス名: Example
範囲: (line 1,col 1)-(line 12,col 1)
継承: []
バリアブル名: foo
範囲: (line 2,col 9)-(line 2,col 11)
型: int
バリアブル名: bar1
範囲: (line 3,col 12)-(line 3,col 15)
型: String
バリアブル名: bar2
範囲: (line 3,col 18)-(line 3,col 21)
型: String
コンストラクター名: Example
範囲: (line 5,col 5)-(line 7,col 5)
メソッド名: GetFoo
範囲: (line 9,col 5)-(line 11,col 5)
返り値の型: int
Example.java
で宣言されている各要素の情報がその順番通りに出力されました。
おわりに
JavaParserで抽象構文木やそれにアクセスするためのVisitorパターンについての知見がいい感じに深まりました。
さて、卒研発表会の準備があるのでそれでは。
Discussion