tokuhirom's Blog

Java の AST を解析できる javaparser がアツい!!!

https://github.com/javaparser/javaparser

javaparser は Java をパースして AST にしてくれるライブラリである。

この手のライブラリは数多あるのだが、ほとんどのものが Java 1.5 ぐらいでメンテナンスが止まっている。 実際このライブラリもメンテナンスが止まっていたのだが、Java 1.8 対応版とし開発が再開されたものだ。

このライブラリはパーサーライブラリであるから、文字列をパースして AST を構築してくれるというものになっている。

実際どのような AST が構築されるのかが気になるところなので、構築された AST をダンプできるツールを groovy で書いた。

#!/usr/bin/env groovy
@Grab('com.github.javaparser:javaparser-core:2.1.0')

import java.io.FileInputStream;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Node;

class Dumper {
  int indent;

  def Dumper() {
    this.indent = 0
  }

  def dumpit(CompilationUnit node) {
    dumpit((Node)node)
    print("\n")
  }
  def dumpit(Node node) {
    print("\n")
    print(" " * indent)
    print(node.getClass().getSimpleName() + ":" + asMap(node));

    this.indent++
    for (Node child: node.getChildrenNodes()) {
      dumpit(child)
    }
    this.indent--
  }

  public Map asMap(o) {
    o.class.declaredFields.findAll { it.modifiers == java.lang.reflect.Modifier.PRIVATE }.
      collectEntries { [it.name, o[it.name]] }
  }
}


def parse(InputStream is) {
  CompilationUnit cu = JavaParser.parse(is);
  new Dumper().dumpit(cu);
}

if (args.length == 0) {
  parse(System.in);
} else {
  FileInputStream fis = new FileInputStream(args[0]);
  try {
    parse(fis);
  } finally {
    fis.close();
  }
}

これを実行すると以下のような結果が得られる。

Java 8 で導入されたラムダ式もちゃんとパースできていることがわかる。

CompilationUnit:[pakage:null, imports:null, types:[public class A {

    public void x() {
        Arrays.stream().map( it->it * 2);
    }
}]]
 ClassOrInterfaceDeclaration:[interface_:false, typeParameters:null, extendsList:null, implementsList:null, javadocComment:null]
  MethodDeclaration:[modifiers:1, typeParameters:null, type:void, name:x, parameters:[], arrayCount:0, throws_:[], body:{
    Arrays.stream().map( it->it * 2);
}, isDefault:false, javadocComment:null]
   VoidType:[:]
   BlockStmt:[stmts:[Arrays.stream().map( it->it * 2);]]
    ExpressionStmt:[expr:Arrays.stream().map( it->it * 2)]
     MethodCallExpr:[scope:Arrays.stream(), typeArgs:null, name:map, args:[ it->it * 2]]
      MethodCallExpr:[scope:Arrays, typeArgs:null, name:stream, args:null]
       NameExpr:[name:Arrays]
      LambdaExpr:[parameters:[ it], parametersEnclosed:false, body:it * 2;]
       Parameter:[type:, isVarArgs:false]
        VariableDeclaratorId:[name:it, arrayCount:0]
        UnknownType:[:]
       ExpressionStmt:[expr:it * 2]
        BinaryExpr:[left:it, right:2, op:times]
         NameExpr:[name:it]
         IntegerLiteralExpr:[:]

さて、これで、どのあたりにどのノードがあって、どのようなアトリビュートを取得可能かが一目瞭然となったので、ビジターを書いて解析してみる。

javaparser では AST をトラバースするためのビジタークラスが用意されているので、これを継承し、処理したいノードを捕まえればよろしい。

例として、クラスとメソッドのリストを出力するコードを書いた。注意すべき点は特に無いが、super.visit を呼び忘れると下位ノードにビジターが回らないので注意。

#!/usr/bin/env groovy
@Grab('com.github.javaparser:javaparser-core:2.1.0')

import java.io.FileInputStream;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.body.*;
import com.github.javaparser.ast.expr.*;
import com.github.javaparser.ast.visitor.*;

class MethodVisitor extends VoidVisitorAdapter<Object> {
    @Override
    public void visit(final ClassOrInterfaceDeclaration n, final Object arg) {
        System.out.println(n.getName());
        super.visit(n, arg)
    }

    @Override
    public void visit(MethodDeclaration n, Object arg) {
        System.out.println('  ' + n.getName());
        super.visit(n, arg)
    }
}

def parse(InputStream is) {
  CompilationUnit cu = JavaParser.parse(is);
  new MethodVisitor().visit(cu, null);
}

if (args.length == 0) {
  parse(System.in);
} else {
  FileInputStream fis = new FileInputStream(args[0]);
  try {
    parse(fis);
  } finally {
    fis.close();
  }
}

さて、ここまで見てくると、javafmt 的なコマンドが欲しくなってくると思う。 javafmt 的なものを実装するにはどうすればよいか。

答えは「何もしなくてよい」である。Node クラスの .toString() メソッドが、そもそも適当にインデントつけてそれなりの見た目で 出力してくれる。Node#toString の実装は、com.github.javaparser.ast.visitor.DumpVisitor であり、これをベースに調整していけば、簡単に好みのフォーマッタを構築できることだろう。

@Grab('com.github.javaparser:javaparser-core:2.1.0')

import java.io.FileInputStream;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;

def parse(InputStream is) {
  CompilationUnit cu = JavaParser.parse(is);
  // prints the resulting compilation unit to default system output
  System.out.println(cu.toString());
}

if (args.length == 0) {
  parse(System.in);
} else {
  FileInputStream fis = new FileInputStream(args[1]);
  try {
    parse(fis);
  } finally {
    fis.close();
  }
}

AST は書き換え可能なので、AST をいじってから書き戻すとか、javapoet 的にコード生成に使うとか、いろいろできそうです。

以上、簡単ですが javaparser の紹介とさせていただきます。