Java代码分析器(二): 使用DOM API操作抽象语法树

上篇博客末尾提到了一棵抽象语法树长什么样子。JDT提供了一套DOM API来让我们顺利地控制这样一棵树。

读完本篇后请继续完成上篇的延伸阅读:http://help.eclipse.org/ 点击JDT Plug-in User Guide -> Programmer's Guide -> JDT Core -> Manipulating Java Code。

各种语法元素用不同的结点类来表示,一些主要的类有:
CompilationUnit: 编译单元,相当于一个.java文件
AbstractTypeDeclaration: 各种类型的声明及定义,如类、接口、枚举、注解。以下几个类都继承自该类
TypeDeclaration: 类或接口
EnumDeclaration: 枚举类
AnnotationInterfaceDeclaration: 注解,即@interface Xxx {...}
MethodDeclaration: 方法
FieldDeclaration: 域 (成员变量)
Modifier: 修饰符,如public, static, volatile等,也包括注解,如@Override
Block: 代码块,也就是花括号{...}所包裹的一段代码
Statement: 语句,是代码块的一部分,也就是以;或}结尾的一段代码
Expression: 表达式,是语句或声明的一部分, 例如a+b, "seg", a.call(b), ((Number) this)等

其中一些是抽象类,它们会有一些具体的子类,列举在抽象类的javadoc中。

结点之间通过对象引用来连接,举个例子,MethodDeclaration主要有这么几个属性:

SimpleName name; //方法名
List<Type> parameters; //方法参数
List<Modifier> modifiers;
List<TypeParameter> typeParameters; //泛型参数
Type returnType;
Block body; //方法体
Javadoc javadoc;

为了掌握JDT的使用方法,我们需要观看其javadoc和源代码,因为文档太缺乏。这部分源代码还是挺容易懂的。
请下载一份源代码,这里有个Eclipse 4.3.1对应的JDT源码包 http://grepcode.com/snapshot/...

元素的组合需要遵守基本的语法规则(不是全部)。当我们用上篇博客的方式读入并解析Java代码时,JDT只会做词法分析和语法分析,而不会做语义分析,因此只能发现符号不合法、语句不完整、括号不匹配之类的简单错误,至于变量未声明、类型不匹配、缺少import、方法未实现之类的错误,则不会发现。语义分析需要Eclipse workspace的支持,因此需要实现一个Eclipse插件,本系列博客不研究这件事;如果费点工夫,也是可以自行实现一定程度的语义分析的,如果有空可以分享一下。

为了更好地理解语法树结构,请读者运行上篇的代码,解析一个真实的.java文件,在debug模式下逐级展开观察CompilationUnit的内部结构。

然后我们来介绍一些语法概念,方便进一步理解:

字面量是表达式的一种,int, boolean, String的常量(以及null, X.class)在语法上都是字面量(literal)。

名字也是表达式的一种,是指变量名、类名、包名等元素,它们在JDT中都属于Name抽象类,分为SimpleName和QualifiedName两种具体类。SimpleName是不带'.'的标识符,如变量名foo;QualifiedName则是带有'.'的合成名字,如包名java.util.List。QualifiedName是由SimpleName组成的。
根据Java语法,标识符只能包含字母、数字、下划线“_”、美元符号“$”,且不得以数字开头。

从上篇最后的代码开始,从CompilationUnit出发跟着对象引用一路走下去,就能分析代码结构了。但是到了代码块一级就要开始面对数不清的甚至有嵌套的语句和表达式了,不得了啊。其实我们可以使用ASTVisitor来遍历所有结点,不用自己硬着头皮一层层去访问。

像下面的代码,就自定义了一个visitor,来遍历所有的方法调用,我们把它应用到CompilationUnit这棵树上(也可以应用到任意一棵子树上):

ASTVisitor visitor = new ASTVisitor() {
    @Override
    public boolean visit(MethodInvocation mi) {
        System.out.println(mi);
        return true;
    }
}
compilationUnit.accept(visitor); //应用到树上

这样它就在这棵树上走了一圈,碰到合适的结点就干活。"return true"的作用是告诉visitor继续前进,如果return false,visitor就会停止前进。

再来溜一段复杂点的代码,我们把visitor的visit方法改一改:

ASTVisitor visitor = new ASTVisitor() {
    @Override
    public boolean visit(MethodInvocation mi) {
        if (mi.getExpression() instanceof ThisExpression) {
            mi.setExpression(null);
        }
        return true;
    }
}
compilationUnit.accept(visitor);
System.out.println(compilationUnit.toString());

如果代码中含有this.doThing()这样的调用,处理后重新输出的代码会变成doThing(),this被去掉了。

[本篇待续]

阅读 5.1k

推荐阅读
编程妙法
用户专栏

实践哲学

29 人关注
36 篇文章
专栏主页
目录