JavaParser 使い方メモ

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

# Hello World
**依存関係**

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

**実装**

```java: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);
        }
    }
}

***********************************************
```

**説明**

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

# VoidVisitor
**実装**

```java: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);
    }
}
```

```java: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!!
```

**説明**

```java: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` には、ノードの種類ごとにコールバックされるメソッドが１００個弱宣言されている
- これらを全て実装し、単純に再帰的にノードを走査していく実装として `VoidVisitorAdapter` クラスが用意されている
- これを継承して独自の `Visitor` を作成する
- 処理したいノードを受け取るメソッドを必要に応じてオーバーライドして、処理を実行する
- 最後に `super.visit()` を呼ぶことで、さらに子供のノードに対して走査が続けられるようになる

```java:Main.java
            CompilationUnit unit = JavaParser.parse(source);
            
            unit.accept(new MyVoidVisitor(), "ARG!!");
```

- `Visitor` は、 `CompliationUnit` クラスの `accept()` に渡すことで使用できる
    - `accept()` メソッドは `Visitable` インターフェースで定義されており、 `CompliationUnit` はそれを実装している
    - ちなみに、 `VoidVisitor` の `visit()` メソッドに走査したいノードを渡す方法もあるが、 `accept()` を使った場合も裏では結局この `visit()` メソッドが呼ばれていて動きは同じになる

```java:CompliationUnitのaccept()メソッドの実装
    @Override
    public <A> void accept(VoidVisitor<A> v, A arg) {
        v.visit(this, arg); // ★VoidVisitor の visit() をそのまま呼んでいる
    }
```

- `accept()` （および `visit()`）メソッドの第二引数に値を渡すと、その値はノードを走査しているあいだずっと連携され続ける
    - たぶん、共通で参照したいデータや処理の委譲先オブジェクトの連携用？

# GenericVisitor
**実装**

```java: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;
    }
}
```

```java: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
```

**説明**

```java: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;
    }
```

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

```java:ParseCommentSample.java
package sample.javaparser;

// a comment

// b comment
public class ParseCommentSample {
    
    // c comment
    
    private String name; // d comment
}
```

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

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

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

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

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

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

# 文字列として出力する
```java: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
        );
    }
}
```

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

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

## 整形の方法を調整する
```java: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)`|インデント１つに使用する文字列を指定する|（スペース４つ）|
|`setPrintComments(boolean)`|コメントを表示するか指定する|`true`|
|`setPrintJavaDoc(boolean)`|Javadoc を表示するか指定する|`true`|
|`setEndOfLineCharacter(String)`|改行で使用する文字列を指定する|`System.getProperty("line.separator")`|

# 構文木を書き換える
```java: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()` など削除メソッドもある



# 参考
- [Home · javaparser/javaparser Wiki](https://github.com/javaparser/javaparser/wiki)

[^1]: [JDTでJavaのソースコード解析を行う - Qiita](http://qiita.com/esplo/items/fa93ab6136e7697ed1d9)