源碼分析工具選型
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是更好的選擇。