Javaのソースコードをトークン化する

Javaのソースコードからトークンを取得してみます。トークンとは、ソースコードから空白やコメントを除去した、識別子名や括弧、数値などのことです。

下はJavaのHelloWorldを出力するソースコードです。

```java
System.out.println("hello world");
```

このソースコードをトークン化すると次のようになります。

```
System
.
out
.
println
(
"hello world"
)
;
```

同時に、トークンの種類（識別子名か文字列リテラルかなど）も特定できます。

## ライブラリを導入する

トークン化にはEclipse JDTを使います。Eclipse JDTはEclipseで使われているJavaのコンパイラーです。

Mavenでは[このサイト](https://search.maven.org/#artifactdetails%7Corg.eclipse.jdt%7Corg.eclipse.jdt.core%7C3.12.3%7Cjar)。

Gradleで導入するには`dependencies`に以下の文を追加してください。

```
compile 'org.eclipse.jdt:org.eclipse.jdt.core:3.12.3'
```

## Scannerの使い方

`org.eclipse.jdt.core.ToolFactory.createScanner`メソッドで`IScanner`インスタンスを生成し、ソースコードをトークン化します。

解析対象のソースコードは、`setSource`メソッドに文字配列（char[]）の形で渡します。次に、`getNextToken()` メソッドで、順次トークンを取得していきます。

あとは下のソースコードを見てください。

```java
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.compiler.IScanner;
import org.eclipse.jdt.core.compiler.ITerminalSymbols;
import org.eclipse.jdt.core.compiler.InvalidInputException;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class JavaTokenizer {
    /**
     * プログラムのエントリポイント
     */
    public static void main(String[] args) throws IOException, InvalidInputException {
        Path sourcePath = Paths.get("src/main/java/JavaTokenizer.java");
        String source = new String(Files.readAllBytes(sourcePath));
        IScanner scanner = ToolFactory.createScanner(true, false, true, "1.9");
        scanner.setSource(source.toCharArray());
        System.out.println("token|start| end |line | ");
        int tokenType;
        while ((tokenType = scanner.getNextToken()) != ITerminalSymbols.TokenNameEOF) {
            int start = scanner.getCurrentTokenStartPosition();
            int end = scanner.getCurrentTokenEndPosition();
            int line = scanner.getLineNumber(start);
            String token = new String(scanner.getCurrentTokenSource());
            String tokenDesc = String.format("%4d |%4d |%4d |%4d | %s", tokenType,
                    start, end, line, token);
            System.out.println(tokenDesc);
        }
    }
}
```

getNextTokenメソッドを呼ぶことで、次のトークンに現在の状態を遷移します。

このプログラムの出力例は下のようになります。

```
token|start| end |line | 
 191 |   0 |   5 |   1 | import
   5 |   7 |   9 |   1 | org
   6 |  10 |  10 |   1 | .
   5 |  11 |  17 |   1 | eclipse
   6 |  18 |  18 |   1 | .
   5 |  19 |  21 |   1 | jdt
   6 |  22 |  22 |   1 | .
   5 |  23 |  26 |   1 | core
   6 |  27 |  27 |   1 | .
   5 |  28 |  38 |   1 | ToolFactory
  64 |  39 |  39 |   1 | ;
```

## ToolFactory.createScannerのパラメータ

### コメントをトークン化する

コメントは普通トークンとして扱いませんが、時にはトークンとして見たいときがあるでしょう。そんなときは第1パラメータである`tokenizeComments`を`true`にします。

```java
IScanner scanner = ToolFactory.createScanner(true, false, true, "1.9");
```

すると、コメントがトークンとして認識されます。

```
1003 | 318 | 352 |  10 | /**
     * プログラムのエントリポイント
     */
```

### 空白をトークン化する
 
空白は普通トークンとして扱いませんが、時にはトークンとして見たいときがあるでしょう。
そんなときは、第2パラメータをtrueに変更するとトークンとして取得できます。
 
```java
IScanner scanner = ToolFactory.createScanner(false, true, true, "1.9");
```
 
出力は以下のようになります。
 
```
1000 | 222 | 223 |   5 | 
```

### ソースコードのバージョンを指定する
上の例では第4パラメータを`1.9`にしています。これを変更すると、解析するソースコードのバージョンを変更できます。
ちなみに指定しない場合、Java1.3がデフォルトで設定されます。

## PMDを用いる

Eclipse JDT を用いたトークン化を説明しましたが、Eclipse JDT はもとはコンパイラーなので非常にJARファイルのサイズが大きいです。また、ソースコード解析に使わない機能も多く、ライブラリ自体の理解もしにくいです。

そのため、ソースコード解析を目的としたライブラリPMDを使うことを検討しましょう。PMDとは、ソースコードを解析してバグやバグっぽい箇所を特定するツールです。以下にトークン化をするプログラムの実装例を示します。（`net.sourceforge.pmd:pmd-java:5.4.1`をライブラリとして追加してください）

```java
import net.sourceforge.pmd.lang.ast.JavaCharStream;
import net.sourceforge.pmd.lang.java.ast.JavaParserTokenManager;
import net.sourceforge.pmd.lang.java.ast.Token;

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class JavaTokenizer {
    public static void main(String[] args) throws IOException {
        Path path = Paths.get("src/main/java/JavaTokenizer.java");
        try (BufferedReader br = Files.newBufferedReader(path)) {
            JavaCharStream javaCharStream = new JavaCharStream(br);
            JavaParserTokenManager javaParserTokenManager = new JavaParserTokenManager(javaCharStream);
            Token token;
            while ((token = javaParserTokenManager.getNextToken()).kind > 0) {
                String desc = String.format("%4d:%-4d %s", token.beginLine, token.beginColumn, token.image);
                System.out.println(desc);
            }
        }
    }
}
```