一.概述
产品线最近在线上出了两个相似的问题:开发人员在写循环代码的时候没有在循环体内使用到循环变量而发生了错误,比如如下代码:
1
List list = getList();
2
for(int i = 0;i < 9 ;i++){
3
System.out.println(list.get(0));
4
}
其实我们在代码中是想写list.get(i)的,为了发现诸如此类的问题,我们需要把它固化成静态代码检查的规则,然后在每次的持续集成中执行。
现在用得比较广的Java静态代码检查工具有Findbugs和PMD,由于最近在研究抽象语法树方面的东西,这次就采用PMD来实现。PMD有两种实现,对于简单的规则我们直接借助已经写好的Xpath规则就能很轻松地达到,对于复杂的规则我们就必须熟悉PMD的api和设计规则了,以下就两种方式进行介绍。
二.用类Xpath写规则
首先下载4.2.5版本的PMD,然后采用命令行的方式进入到bin目录,并运行 designer命令,我们可以看到如下图:
左上角你需要贴入的代码,左下角是生成的抽象语法树,右上角是设计的xpath规则,右下角是符合xpath查询条件的内容。对于以下一段代码:
1
public class a {
2
int hello;
3
int world;
4
5
private void run() {
6
int one = 1;
7
int two = 2;
8
}
9
}
生成的语法树如下:
如果你需要找到所有的变量定义,那么使用//VariableDeclaratorId就ok,如果要找到本地变量的定义,那么使用//LocalVariableDeclaration,这里的xpath以//开头表示一个广泛匹配,表示只要是某个节点就ok,//后面跟的变量是抽象语法树中的节点。
如果要做这么一条规则:找出所有if中存在空语句的代码,比如如下代码:
1
public class Foo {
2
void bar(int x) {
3
if (x == 0) {
4
// empty!
5
}
6
}
7
}
那么我们的规则可以写为//IfStatement/Statement[EmptyStatement or Block[count(*) = 0]],表示找出if中的statement中的空声明或者语句数量为0的代码。对于所有写好的规则我们都必须使用多种源代码进行反复测试,然后在几个真实项目中试用一下发现没有问题才正式加入静态代码检查工具中。
三.调用API写规则
对于一个复杂的规则,我们没法用xpath来表达,那就必须调用PMD的API了。对于PMD和Findbugs这种检查工具都是采用访问者模式,示意图如下:
这里的ObjectStructure就是整个语法树,Element就是语法树中的各个节点对象,比如TypeDeclaration(类型声明)节点、FieldDeclaration(字段声明)节点等等,Visitor观察者即为我们的规则。当我们的规则写好后,由PMD负责调用规则来观察所有语法树中的节点,如果发现观察到的内容与规则不符就会将信息记录到PMD的Report里。
对于我们概述中提到的需求,就是采用调用API的方式来实现,我们只需要继承AbstractJavaRule(这个类就相当于Visitor接口的抽象类实现,里面有些默认的行为)。
代码如下:
1
package net.sourceforge.pmd.rules;
2
3
import java.util.List;
4
5
import net.sourceforge.pmd.AbstractJavaRule;
6
import net.sourceforge.pmd.ast.ASTClassOrInterfaceType;
7
import net.sourceforge.pmd.ast.ASTForInit;
8
import net.sourceforge.pmd.ast.ASTForStatement;
9
import net.sourceforge.pmd.ast.ASTName;
10
import net.sourceforge.pmd.ast.ASTStatement;
11
import net.sourceforge.pmd.ast.ASTVariableDeclaratorId;
12
13
public class ForVariableNotUseRule extends AbstractJavaRule {
14
15
public Object visit(ASTForStatement node, Object data) {
16
if (!isUsed(node)) {
17
addViolation(data, node, "the varable in for statement is not used");
18
}
19
return data;
20
}
21
22
private boolean isUsed(ASTForStatement node) {
23
Boolean isUsed = false;
24
ASTForInit forInit = node.getFirstChildOfType(ASTForInit.class);
25
if (null == forInit) {
26
isUsed = true;
27
} else {
28
ASTClassOrInterfaceType variableType = forInit.getFirstChildOfType(ASTClassOrInterfaceType.class);
29
if(variableType != null && "Iterator".equals(variableType.getImage())){
30
return true;
31
}
32
33
ASTVariableDeclaratorId variableDeclaratorId = forInit
34
.getFirstChildOfType(ASTVariableDeclaratorId.class);
35
String variableName = variableDeclaratorId.getImage();
36
37
ASTStatement statement = node
38
.getFirstChildOfType(ASTStatement.class);
39
List names = statement.findChildrenOfType(ASTName.class);
40
for (ASTName name : names) {
41
if (variableName.equals(name.getImage())) {
42
isUsed = true;
43
}
44
}
45
}
46
return isUsed;
47
}
48
}
主要逻辑在isUsed()方法里面,我们首先找到for循环中初始化的语句,然后找到初始化变量的名称。接着我们分析初始化里面是不是使用迭代器方式使用,如果是我们认为不会出现之前提到的问题。最后再判断初始化变量是否在后续的for循环代码中使用到。
PS:开发中我们如果不用到for中的变量我们应该使用迭代器或者foreach,这是一种更加优雅的方式。
四.加入到静态代码检查工具
规则写好后,最后我们要做的工作是把它加入到静态检查工具中,我们修改pmd rule文件夹下的pmd.xml文件,在里面加上:
1
2
The rule check the variable defined in the "for cycle" and never used in the next code. It is a bad code and lead to a bug, even so you should use foreach.
3
2
4
5
6
List list = new ArrayList();
7
for(int i = 0;i < 9 ;i++){
8
System.out.println(list.get(0));
9
}
10
]]>
11
12
五.总结 以上就是对如何增加静态代码检查规则进行一个简单的介绍,其实大部分需求PMD或者Findbugs都帮我们做好了,如果确实需要新增一个规则的时候我们需要确认是不是已经在已有规则里面了,而不必重复造轮子。另外分析Java抽象语法树其实还可以完成非常多的功能,比如Java源代码比对、源代码生成工具等,Eclipse里面多处用到了这些技术。