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

  • 10
    いいね
  • 0
    コメント

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

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

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

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

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

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

ライブラリを導入する

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

Mavenではこのサイト

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

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

Scannerの使い方

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

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

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

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パラメータであるtokenizeCommentstrueにします。

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

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

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

空白をトークン化する

空白は普通トークンとして扱いませんが、時にはトークンとして見たいときがあるでしょう。
そんなときは、第2パラメータをtrueに変更するとトークンとして取得できます。

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をライブラリとして追加してください)

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);
            }
        }
    }
}