源码分析工具选型
1. 目前各种主流源码分析工具简单介绍
1.1 checkstyle
checkstyle产生于2001年,是以antlr作为java语法分析器的静态源码分析工具。通过checkstyle的xml配置文件可指定源码分析规则。通过继承checkstyle自身的Check可实现新的代码检查逻辑。另外继承AbstractFileSetCheck可实现除java以外的其它编程语言的检查规则,不过checkstyle封装的antlr只能分析java语法,而且没有封装其它的语法分析器,因此如果要用checkstyle检查其它语言的代码需要封装或实现相应语言的语法分析工具。
对于java代码,是基于语法树的检查,整个检查过程就是遍历语法树的过程
经阅读checkstyle源码,确定整个检查过程是单线程执行的。
1.1.1 适用范围
各种纯文本文件,除java代码外的其它文本需要提供提供语法分析工具和检查规则。
1.1.2 checkstyle原理(以下内容都以java源文件为例,其它语言的检查过程除需要自己提供语法分析工具之外与分析java文件一致)
1)checkstyle会遍历指定目录的文件,针对每个java文件使用antlr获得一棵语法树。Class定义的语法树示例如下:
public class MyClass implements Serializable{
}
+--CLASS_DEF
|
+--MODIFIERS
|
+--LITERAL_PUBLIC (public)
+--LITERAL_CLASS (class)
+--IDENT (MyClass)
+--EXTENDS_CLAUSE
+--IMPLEMENTS_CLAUSE
|
+--IDENT (Serializable)
+--OBJBLOCK
|
+--LCURLY ({)
+--RCURLY (})
2)checkstyle会从整个语法树的根开始遍历整个语法树。
3)以类定义检查为例,checkstyle会查找有没有指定针对类定义的代码检查规则,如果有,checkstyle会遍历所有类定义相关代码检查规则,并把类定义(CLASS_DEF)语法树的DetailAST对象传入Check子类的visitToken(DetailAST ast);执行类定义相关的语法检查。整个DetailAST语法树包含了一个java文件的全部信息,也就是能够通过一个类定义语法树获得一个类的全部内容。如果有多个类定义检查规则,checkstyle会依次执行每个visitToken(DetailAST ast)。如果没有指定相关类定义检查规则,就忽略类定义检查。
4)按照上面所描述的,检查规则Check对象可以从DetailAST对象得到从当前语法树节点得到这个节点与它的所有子节点的全部信息。例如,如果当前DetailAST对象表示一个LITERAL_IF对象,而且它对应的if语句后面还有else语句,那么就能通过这个DetailAST对象得到相应的if语句和else语句的全部信息。
以如下if语句作为说明:
if(optimistic){
message = "half full";
}else{
message = "half empty";
}
+--LITERAL_IF (if)
|
+--LPAREN (()
+--EXPR
|
+--IDENT (optimistic)
+--RPAREN ())
+--SLIST ({)
|
+--EXPR
|
+--ASSIGN (=)
|
+--IDENT (message)
+--STRING_LITERAL ("half full")
+--SEMI (;)
+--RCURLY (})
+--LITERAL_ELSE (else)
|
+--SLIST ({)
|
+--EXPR
|
+--ASSIGN (=)
|
+--IDENT (message)
+--STRING_LITERAL ("half empty")
+--SEMI (;)
+--RCURLY (})
上面的if语句与紧接着的语法树一一对应,表示这个if语句的DetailAST对象包含了这个语法树的全部信息,能够通过这个DetailAST对象遍历整个if语法树。同时可以通过遍历这个语法树用来判断相应的if语句体是不是空,有没有else子句。同样的方式可以用来判断循环体是不是空,方法体是不是空,try/catch/finally是不是空,switch语句包含多少case和有没有default子句、是否有未被调用的private方法或属性等。
1.1.3 checkstyle预定义检查规则与配置文件
部分预定义规则如下所示:
MethodLength 方法最大行数检查,超出设定值按错误处理
ParameterNumber方法与构造器参数数目检查,超出设定值按错误处理
ParameterName参数名称格式检查,不符合指定正则表达式的参数名称按错误处理
PackageName检查包命名,不符合指定正则表达式的包名按错误处理
TypeName检查接口与类名,不符合指定正则表达式的接口名与类名按错误处理
MethodName 检查方法名,不符合指定正则表达式的方法名按错误处理
LocalFinalVariableName 检查局部final变量名与catch参数,不明白意义
LocalVariableName 检查非final局部变量与catch参数,不明白意义
MemberName 检查成员属性是否符合指定正则表达式
AvoidStarImport 检查是否用*导入类
AvoidStaticImport 检查是否有静态导入,和是否导入lang包类
IllegalImport 检查是否有非法导入,默认拒绝导入所有sub.*
RedundantImport 检查是否有重复导入
UnusedImports 检查是否有未使用的导入
AvoidNestedBlocks 检查不需要的代码块嵌套
NeedBraces 检查是否需要花括号
ArrayTrailingComma 检查数组初始化是否以逗号结束
EqualsAvoidNull 检查字符串调用equals(),点号左边是否可能为空
MissingSwitchDefault 检查switch是否有default,没有被视为错误
ModifiedControlVariable 检查循环控制变量是否在代码块中被修改
RedundantThrows 检查是否有重复抛出的异常
SimplifyBooleanExpression 检查是否有过度复杂的boolean表达式,不知道多复杂算过度复杂
StringLiteralEquality 检查是否用== !=比较字符串
NestedIfDepth 检查代码块嵌套深度是否超过指定值
NestedTryDepth 检查try嵌套深度是否超过指定值
IllegalCatch 检查是否catch了不能接受的错误
IllegalThrows 检查是否抛出了未声明的异常
PackageDeclaration 检查是否声明了package
IllegalType 检查未使用过的类
MissingCtor找出没有定义的构造函数的类,检查类依赖
<!--对方法实行长度测试定义,如果长度长于20行的就按出错处理--> <module name = "MethodLength"> <property name = "max" value = "20"/> <property name = "tokens" value ="METHOD_DEF"/> <!--把单行注释和空行除掉--> <property name = "countEmpty" value = "false"/> </module> <!--检查方法和构造函数的参数个数,现在以10个参数个数为例--> <module name = "ParameterNumber"> <property name = "max" value = "10"/> <property name = "tokens" value = "METHOD_DEF"/> </module> <!--******Naming Conventions******--> <!--检查参数的命名格式--> <module name = "ParameterName"> <property name = "format" value = "^[a-z][a-zA-Z0-9]*$"/> </module> <!--检查包命名--> <module name = "PackageName" > <property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/> </module> <!--检查类名和接口名--> <module name = "TypeName"> <property name = "format" value = "^[A-Z][a-zA-Z0-9]*$"/> <property name = "tokens" value = "CLASS_DEF,INTERFACE_DEF"/> </module> <!--检查方法名--> <module name = "MethodName"> <property name ="format" value = "^[a-z][a-zA-Z0-9]*$"/> </module> <!--检查局部的final类型变量名,包括catch的参数--> <module name = "LocalFinalVariableName"> </module> <!--检查局部的非final类型变量名,包括catch的参数--> <module name = "LocalVariableName"> </module> <!--检查非静态变量--> <module name = "MemberName"> <property name="format" value="^m[A-Z][a-zA-Z0-9]*$"/> </module> <!--*****Imports******--> <!--检查是否有使用*进行import--> <module name = "AvoidStarImport"> </module> <!--检查是否有静态的import,比如是否导入了java.lang包中的内容--> <module name = "AvoidStaticImport"> </module> <!--是否import了违法的包,默认拒绝import所以sun.*包--> <module name= "IllegalImport"> </module> <!--检查是否有重复的import--> <module name = "RedundantImport"> </module> <!--检查import而未有使用过的import--> <module name ="UnusedImports"> </module> <!--******Block Checks******--> <!--检查是否需要大括号。主要是在if,else时的情况.(貌似没这个必要,可以省略该项)--> <module name = "NeedBraces"> </module> <!--检查不需要的嵌套’{}’。--> <module name = "AvoidNestedBlocks" /> <!--*********Coding**********--> <!--检查数组初始化是否以逗号结束。--> <module name = "ArrayTrailingComma"/> <!--检查一个可能为null的字符串是否在equals()比较的左边。--> <module name = "EqualsAvoidNull"/> <!--检查switch语句是否有default的clause--> <module name = "MissingSwitchDefault"/> <!--检查循环控制的变量是否在代码块中被修改。--> <module name = "ModifiedControlVariable"/> <!--检查是否有被重复抛出的异常。--> <module name = "RedundantThrows"> <property name = "allowUnchecked" value = "true"/> </module> <!--检查是否有过度复杂的布尔表达式。--> <module name = "SimplifyBooleanExpression"/> <!--检查字符串是否有用= =或!=进行操作。--> <module name = "StringLiteralEquality"/> <!--检查嵌套的层次深度是否超过最大值3。--> <module name = "NestedIfDepth"> <property name = "max" value = "3"/> </module> <!--检查try的层次深度是否超过2--> <module name = "NestedTryDepth"> <property name = "max" value = "2"/> </module> <!--检查是否catch了不能接受的错误。--> <module name = "IllegalCatch"/> <!--检查是否抛出了未声明的异常。--> <module name = "IllegalThrows"/> <!--检查类中是否有声明package。--> <module name = "PackageDeclaration"/> <!--检查未使用过的类。--> <module name = "IllegalType"> <property name = "ignoredMethodNames" value = "getInstance"/> </module> <!--找出没有定义的构造函数的类,检查类依赖--> <module name = "MissingCtor"/>
1.1.4 输出检查结果
Checkstyle默认定义两种输出方式
1) xml格式的输出,可以把生成的xml文件用xslt转换成html,checkstyle不提供转换api
2) 纯文本格式的输出,基于纯文本格式,可以空格分隔、制表符分隔、逗号分隔方式输出
输出方式和输出文件在传入Main.main(String[])的参数中指定
分别是-f plain –c OUTPUT_PATH
在实际的检查逻辑中当需要输出检查结果时调用Check.log()。Log有两个重载
public final void log(int aLineNo,
int aColNo,
java.lang.String aKey,
java.lang.Object... aArgs)
public final void log(int aLine,
java.lang.String aKey,
java.lang.Object... aArgs)
参数:
aLineNo:出错行号
aColNo:出错列号
aKey:错误信息文本
aArgs:错误详细内容,这是个不定长参数可传入任意数目任意类型的对象
1.1.5 扩展checkstyle
扩展checkstyle有三种方式。
1) 实现Check子类,用于实现实际的检查逻辑,
2) 实现Filter接口或FilterSet子类,用于决定当前的AuditEvent是否需要通知listener
3) 实现AuditListener,用于执行检查开始、结束、发现错误等情况要做的工作。一般检查日志和检查结果从listener输出
默认的检查结果输出方式和Filter已经有相应的实现,个人认为预定义listener和默认Filter足够使用,不需要扩展。
需要扩展的就是Check,可通过实现Check的子类用于增加新的代码检查逻辑。
1.2. PMD
PMD产生于2002年,是以JAVACC作为java语法分析器的静态源码分析工具。PMD同样是首先把java源码解析成语法树(pmd用xml格式维护语法树),然后遍历语法树进行代码检查,只不过java语法分析器是javacc。
拥有丰富的命令行参数,可指定基于哪个jdk版本进行检查(默认jdk1.5),待检查文件字符集,检查报告输出格式
目前支持text xml html nicehtml csv ideaj parapri emacs yahtml summaryhtml vbhtml。如果指定nicehtml,会使用默认或指定的xslt格式化检查报告,格式化后的文件格式是一个html文件
如果要扩展PMD,需要继承AbstractRule,或使用xpath检查逻辑。个人感觉使用xpath遍历语法树很简单,而且pmd提供了api可以xml形式返回整个语法树,能从xml文本迅速了解语法树结构,从而快速构造出需要的xpath。
PMD与checkstyle一样,默认只能检查java源码,因为它只包含了javacc语法分析器,而javacc只能分析java源码;而且没有找到检查其它代码的扩展点。
PMD有些预定义规则过于严格,导致这些规则并不太实用,比如PMD认为in是过短的变量名,catch子句不能为空等。
在自定义规则中向检查报告输出信息时,输出的汉字都转化成了类似&eb21;的字符串,只好改成用英文。如果要输出汉字,需要修改源码。
许多有用的参数没有在官方文档公布出来,需要看源码才能解决。比如检查结果的输出文件路径就没有公布,还有上面提到的报告生成格式只公布了前四种。
经阅读源码和api,确定PMD是多线程执行的,所有规则检查完成之后再由主线程输出检查报告。线程数==cpu数。按照这样的模式消耗的内存有些大。具体方法是首先获取当前系统cpu数,然后根据这个数字创建一个线程数固定的线程池;如果线程池创建失败就单线程执行。
1.2.1.适用范围
1)适用于java源码分析,还没有发现分析其它语言的扩展api。
2)可以检查jsp。PMD按照严格的xhtml规范检查jsp中的html,并用指定规则集检查jsp中的java代码,但是如果要检查el表达式就需要自定义检查规则。
3)自带多语言冲突代码检查器(CPD),并且可扩展它不支持的语言。不过通过阅读示例,个人感觉似乎并不实用。下图是JDK 1.4 java.*的检查结果。
从图上可以看出CPD认为HashMap与WeakHashMap存在重复代码。
如果能成功编写出针对CPD的js分析器,就可以用它查找指定目录下的所有js重名函数,但是显然这一方法并不太适用于我们,它并不能检查出同一html所引用的javascript内的重复代码。
1.2.2.PMD原理
1)PMD会遍历指定目录的文件,针对每个java文件使用javacc获得一棵语法树。以下面的类定义为例:
class Example {
void bar() {
while (baz)
buz.doSomething();
}
}
它生成的语法树如下所示:
CompilationUnit
TypeDeclaration
ClassDeclaration:(package private)
UnmodifiedClassDeclaration(Example)
ClassBody
ClassBodyDeclaration
MethodDeclaration:(package private)
ResultType
MethodDeclarator(bar)
FormalParameters
Block
BlockStatement
Statement
WhileStatement
Expression
PrimaryExpression
PrimaryPrefix
Name:baz
Statement
StatementExpression:null
PrimaryExpression
PrimaryPrefix
Name:buz.doSomething
PrimarySuffix
Arguments
2)所有的规则都有如下继承关系,所有的检查规则都是它们的子类。要检查java源码需要继承AbstractRule,AbstraceJavaRule定义了许多visit方法,这些visit方法的参数都是SimpleJavaNode的子类,这些子类对象表示语法树中的每个节点,要检查哪个节点就实现相应的visit方法
3)可以SimpleJavaNode利用SimpleNode继承的方法得到当前语法节点的子节点,进而实现检查规则。还可以把语法树转换成org.w3c.dom.Document,利用解析xml的方式检查代码。要想利用PMD检查其它语言的代码必须把相应语言解析成PMD能识别的语法树,这个语法分析器必须完美契合PMD的api。
1.2.3.PMD预定义规则和配置文件
PMD有许多预定义规则,不同的规则集合在一起开成规则集。执行检查时就是根据指定规则集进行检查,可以通过已有规则集定义新的规则集,可以在配置文件中指定新规则集包含哪些规则集中的哪些具体规则或去掉某些规则集中的规则。
下面是配置文件的一部分和部分规则集。
<?xml version="1.0"?> <ruleset name="Custom ruleset" xmlns="http://pmd.sf.net/ruleset/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd" xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd"> <description> This ruleset checks my code for bad stuff </description> <!—使用整个strings规则集 --> <rule ref="rulesets/strings.xml"/> <!—使用下列规则集中的某一个 --> <rule ref="rulesets/unusedcode.xml/UnusedLocalVariable"/> <rule ref="rulesets/unusedcode.xml/UnusedPrivateField"/> <rule ref="rulesets/imports.xml/DuplicateImports"/> <rule ref="rulesets/basic.xml/UnnecessaryConversionTemporary"/> <!-- We want to customize this rule a bit, change the message and raise the priority --> <rule ref="rulesets/basic.xml/EmptyCatchBlock" message="Must handle exceptions"> <priority>2</priority> </rule> <!-- Now we'll customize a rule's property value --> <rule ref="rulesets/codesize.xml/CyclomaticComplexity"> <properties> <property name="reportLevel" value="5"/> </properties> </rule> <!—使用braces规则集,但把WhileLoopsMustUseBraces排除在外 --> <rule ref="rulesets/braces.xml"> <exclude name="WhileLoopsMustUseBraces"/> </rule> </ruleset>
1.2.4 输出检查结果
PMD有text xml html nicehtml四种输出结果,输出格式都在PMD简单介绍部分提到过了。下面说下如何在自定义规则中输出检查结果。
AbstractRule从AbstractJavaRule继承了四个方法用于生成检查报告,一个检查结果一个报告,对应输出文件的一行。具体方法说明参考api文档
protected void addViolation(java.lang.Object data, Node node, java.lang.Object[] args)
Adds a violation to the report.
protected void addViolation(java.lang.Object data, SimpleNode node)
Adds a violation to the report.
protected void addViolation(java.lang.Object data, SimpleNode node, java.lang.String embed)
Adds a violation to the report.
protected void addViolationWithMessage(java.lang.Object data, SimpleNode node, java.lang.String msg)
Adds a violation to the report.
1.2.5 扩展PMD
继承AbstractRule,重写相应的visit方法即可。如果要生成报告调用上面的提到的addViolation方法。
所有的visit方法的第一个参数都是SimpleNode的子类,可用SimpleNode. findChildNodesWithXPath(String xpath)访问语法树。还有其它的语法树访问方法,这一个我认为是最方便的。
1.3.findbugs
Findbugs产生于2003年,是基于bcel库通过扫描字节码完成代码检查的代码检查工具。只要是能编译成字节码的源文件都可用findbugs检查,但是需要对bcel库和字节码有相当了解。
1.3.1.适用范围
由于findbugs是分析的字节码,理论上只要是字节码就能检查。
1.3.2.原理
没有找到相关的文档介绍,官方网站的介绍也只是简单说明下用法。
1.3.3.预定义规则
翻遍了官方文档也没找到这方面比较有用的介绍,用搜索引擎也没找到。
1.3.4.扩展
只找到一篇相关文章。下面是文章内的代码,作用是检查代码中是不是有System.out和System.error
直接在findbugs目录中增加类
package edu.umd.cs.findbugs.detect;
import org.apache.bcel.classfile.Code;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.bcel.OpcodeStackDetector;
/**
*@authorbo
*这个规则类用于判断System.out和System.error这种情况
*/
public class ForbiddenSystemClassextendsOpcodeStackDetector{
BugReporter bugReporter;
public ForbiddenSystemClass(BugReporter bugReporter) {
this.bugReporter= bugReporter;
}
/**
* visit方法,在每次进入字节码方法的时候调用
*在每次进入新方法的时候清空标志位
*/
@Override
public void visit(Code obj) {
super.visit(obj);
}
/**
*每扫描一条字节码就会进入sawOpcode方法
*
*@paramseen 字节码的枚举值
*/
@Override
public void sawOpcode(intseen) {
if(seen ==GETSTATIC) {
if(getClassConstantOperand().equals("java/lang/System")
&& (getNameConstantOperand().equals("out")
|| getNameConstantOperand().equals("error"))) {
BugInstance bug =new BugInstance(this,"ALP_SYSTEMCLASS",NORMAL_PRIORITY)
.addClassAndMethod(this)
.addSourceLine(this, getPC());
bug.addInt(getPC());
bugReporter.reportBug(bug);
}
}
}
}
修改etc目录配置文件findbugs.xml和message.xml
不支持中文注释。
在findbugs.xml增加内容。
<Detectorclass="edu.umd.cs.findbugs.detect.ForbiddenSystemClass" speed="fast" reports="ALP_SYSTEMCLASS" hidden="false"/> <BugPatternabbrev="LIANGJZFORBIDDENSYSTEMCALSS"type="ALP_SYSTEMCLASS"category="EXPERIMENTAL" />
Message.xml增加:
<Detectorclass="edu.umd.cs.findbugs.detect.ForbiddenSystemClass"> <Details> <![CDATA[ <p>category:detector find System.out/System.error <p>please use log4j ]]> </Details> </Detector> <BugPattern type="ALP_SYSTEMCLASS"> <ShortDescription>short desc:System.out/error</ShortDescription> <LongDescription>class={0},method {1}long desc:System.out,please use log4j</LongDescription> <Details> <![CDATA[ <p>detail info see log4j document</p> ]]> </Details> </BugPattern>
1.4.其它java代码检查工具
Lint4j,基于antlr的代码检查工具,文档太少,没深究
Hammurapi,基于antlr和bcel的代码检查工具。需要预安装。它包含很多扩展包,用于扩展检查规则。中文相关文档只有几篇100字左右的介绍,上官网看了下,也是大略的介绍,没有找到介绍扩展相关的信息。
1.5.js检查工具
上网找到几个检查工具,都是js实现,运行在浏览器上的。只有jslint可能满足我们的要求。
Jslint包含js版和java版。Java版名叫jslint4java。
Js版的jslint可以运行在浏览器上也可以运行在rhino上面。(rhino是mozilla基于java开发的js解释器,也可以把js编译成字节码运行。)
下面介绍下jslint4java。
1.4.1.适用范围
检查js源码,一次只能检查一个js文件;但是可以通过封装以类似递归调用的方式检查多个文件。当然网上没有这样的介绍,是我个人的理解。
1.4.2.原理
分析js源码,构造js语法树。并指出js错误。
1.4.3.扩展
JSLint jslint= new JSLintBuilder().fromDefault();
JSLintResult result=jslint.lint(String systemId,String js);
String report=Jslint.report();//返回值是html格式的报告
可通过JSLintResult的方法,得到js源码的相关信息
可以通过遍历List<JSFunction> JSLintResult.getFunctions();判断当前js文件内函数是否有重复定义。
另外同js文件内的重名函数、花括号不匹配等错误检查在jslint4java库也有默认实现。
2. 综述
综合以上论述,checkstyle pmd findbugs各有优缺,三方预定义规则都不能覆盖全部代码检查范围,有些规则过于严格,容易产生误报。三方文档都不是很全面,不论是不是官方的;相对来说Checkstyle和pmd文档相对丰富一些,扩展findbugs的文档几乎没有。曾试图编写checkstyle的扩展demo,但是花了很久也没搞懂怎么遍历语法树,只好放弃;不过我成功制作了一个pmd的扩展demo,基于xpath遍历语法树检查log4j应用规范。另外个人感觉,pmd的模块化做的更好一些,语法分析、构造语法树、检查规则、输出报告等等都在独立的模块内执行。至于js检查工具,由于除jslint4java之外,其它js检查工具都必须运行在浏览器上,也就没有深究,只简述了下jslint4java的使用扩展。
通过这次编写demo,最后的结果出乎我的预料。刚开始我认为checkstyle是最容易扩展和最易上手的,但是现在我认为pmd是更好的选择。