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 の紹介とさせていただきます。