🗾

卒業研究にJavaParserを使ったのでその紹介をする

2022/02/07に公開

はじめに

私の卒業研究では、Javaのソースコードからクラス図を生成するツールの開発を行ないました。
クラス図をコードから生成するツールは既にありそうですが、要件上クラス図が画像だと問題があったためクラス図を手作業でクソめんどいけど作ることになりました。

クラス図を生成するには、それに必要な情報をソースコードから抜き出さなければなりません。
そのためにJavaParserという構文解析ライブラリを使ったのですが、これを紹介している記事が少なかったので紹介しようと思い、この記事を執筆しました。

JavaParser

JavaParserはJavaのソースコードを解析するライブラリです。
JavaParserによって解析されたコードは、一旦抽象構文木に変換されます。
ユーザーはVisitorパターンを用いることで、抽象構文木中の各ノードにアクセスします。

実装

Main.java
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()を開始します。

Visitor.java
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()でその名前を取得できます。

FieldDeclarationvisit()では、他のとは違ってforで回しています。
Javaでは、以下のようにフィールドを1行で複数個定義できます。

int a, b;  // Field
//  ^  ^ Variable

JavaParserでは1行で定義してあるのをField、個々をVariableとしており、今回は個別のVariableがほしいためforで回しています。

出力

そんなこんなで、次の入力ファイルの例

Example.java
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