JavaParser 使い方メモ

  • 7
    Like
  • 0
    Comment

JavaParser とは

  • その名の通り、 Java のソースコードを構文解析するライブラリ
  • ソースの解析結果を抽象構文木(AST)として取得でき、 Visitor パターンなどの方法で解析結果にアクセスできる
  • Java のソース解析のライブラリといえば Eclipse JDT を使う方法がある1
    • JDT は Eclipse プラグインのために使うのが本来の目的なのに対して、 JavaParser は純粋に Java の構文解析が目的となっている
    • 必要なライブラリも jar 1つ(800KB ほど)なので、導入は楽っぽい
  • Java 9 に対応しているらしい
  • API に Optional が使用されているので、少なくとも Java 8 以上でしか使えない

Hello World

依存関係

build.gradle
dependencies {
    compile 'com.github.javaparser:javaparser-core:3.3.0'
}

実装

Main.java
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);
        }
    }
}

***********************************************

説明

  • JavaParserparse() メソッドで Java ソースコードの解析を実行できる
  • 解析結果は CompilationUnit という型で返る
    • CompilationUnitNode クラスを継承している

VoidVisitor

実装

MyVoidVisitor.java
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);
    }
}
Main.java
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!!

説明

MyVoidVisitor.java
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() を呼ぶことで、さらに子供のノードに対して走査が続けられるようになる
Main.java
            CompilationUnit unit = JavaParser.parse(source);

            unit.accept(new MyVoidVisitor(), "ARG!!");
  • Visitor は、 CompliationUnit クラスの accept() に渡すことで使用できる
    • accept() メソッドは Visitable インターフェースで定義されており、 CompliationUnit はそれを実装している
    • ちなみに、 VoidVisitorvisit() メソッドに走査したいノードを渡す方法もあるが、 accept() を使った場合も裏では結局この visit() メソッドが呼ばれていて動きは同じになる
CompliationUnitのaccept()メソッドの実装
    @Override
    public <A> void accept(VoidVisitor<A> v, A arg) {
        v.visit(this, arg); // ★VoidVisitor の visit() をそのまま呼んでいる
    }
  • accept() (および visit())メソッドの第二引数に値を渡すと、その値はノードを走査しているあいだずっと連携され続ける
    • たぶん、共通で参照したいデータや処理の委譲先オブジェクトの連携用?

GenericVisitor

実装

MyGenericVisitor.java
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;
    }
}
Main.java
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

説明

MyGenericVisitor.java
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;
    }
  • VoidVisitorvisit() には戻り値がなかったが、 GenericVisitorvisit() には戻り値がある
  • 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() メソッドの引数で受け取れるのは、これらのうち末端のクラスになる(TypeReferenceType など階層の途中のクラスを受け取るメソッドは無い)。

だいたいクラス名からコードのどの要素に対応しているかは想像できるが、一部想像できないものがあるので、その辺だけピックアップして動作を確認する。

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 構文とは異なるルールで扱われる。

ParseCommentSample.java
package sample.javaparser;

// a comment

// b comment
public class ParseCommentSample {

    // c comment

    private String name; // d comment
}

解析対象のクラス。
このクラスをパースしてコメントを取得してみる。

Main.java
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 を次のように実装して動作を確認してみる。

Main.java
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() で取得することができる
  • LineCommentBlockComment を受け取る visit() メソッドが呼ばれるのは、どこかのノードに付属しているコメントだけになる
  • 孤立したコメントも処理したい場合は、 getOrphanComment() を使うか、もしくは getAllContainedComments() を使えば全てのコメントが取得できる

ちなみに、1つのノードに紐づくコメントは1つだけなので(getComment() は単一のコメントしか返さない)、

// a comment
String s = "xxx"; // b comment

のようになっている場合は // b comment が変数宣言に紐づくコメントとして扱われ、 // a comment は孤立コメントとなる(そういうルール)。

文字列として出力する

ToStringSample.java
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
        );
    }
}

わざと改行を入れてフォーマットに特徴をつけている。

Main.java
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() すると、入力のままの形ではなく、ある程度整形された文字列になる
  • また、コメントは消されることなく出力される

整形の方法を調整する

Main.java
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")

構文木を書き換える

Main.java
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() など削除メソッドもある

参考