使用PHP-Parser產生AST抽象文法樹,php-parserast
0、前言
最近項目的流程逐漸清晰,但是很多關鍵性的技術沒有掌握,也只能一步一步摸索。
由於要做基於資料流分析的靜態程式碼分析,所以前端的工作如:詞法分析、文法分析必不可少。Yacc和Lex什麼的就不再考慮了,查了一天的資料,發現兩款比較適合,一款是Java下的ANTLR,另一款是專門做PHP AST產生的PHP-Parser。
ANTLR是編譯原理領域比較著名的工具了,相對於Yacc和Lex,更加實用。但是對PHP的文法檔案只有一個,折騰了半天才產生調通,發現不太適合,對於”$a=1”產生tokens竟然是[$,a,=,1],無法識別assignment,做得過於粗糙,令人無比失望。
相比之下,PHP-Parser更加專業一些,畢竟專註PHP的詞法、文法分析工作。
1、介紹
PHP-Parser的項目首頁是https://github.com/nikic/PHP-Parser。可以對多版本的PHP進行完美解析,產生一顆抽象文法樹。
對於詞法分析,PHP有個內建函數token_get_all()可以用來擷取TOKENS,作為文法分析的輸入,這個開源項目也是用的token_get_all()產生的token流。
2、安裝
安裝也很簡單,這裡我是使用的PHP中的包管理工具composer添加的,在項目目錄中執行以下命令即可:
php composer.phar require nikic/php-parser
如果沒有下載Composer,應該先執行下面的命令:
Curl -s http://getcomposer.org/installer | php
3、產生AST
使用composer添加php-parser之後,就可以方便使用。
首先介紹一下PHP-Parser中定義的一些節點類型:
(1)PhpParser\Node\Stmt是語句節點,不帶任何返回資訊(return)的結構,如賦值語句”$a = $b” ;
(2)PhpParser\Node\Expr是運算式節點,可以返回一個值的語言結構,如$var和func()。
(3)PhpParser\Node\Scalar是常量節點,可以用來表示任何常量值。如’string’,0,以及常量運算式。
(4)還有一些節點沒有包括進去,如參數節點(PhpParser\Node\Arg)。
一些節點類的名稱使用了底線,這是為了避免和PHP關鍵字衝突。
PHP-parser的HelloWorld程式如下,該程式碼片段會產生AST:
輸出結果為:
<span style="font-size:12px;">Array( [0] => PhpParser\Node\Stmt\Echo_ Object ( [subNodes:protected] => Array ( [exprs] => Array ( [0] => PhpParser\Node\Scalar\String Object ( [subNodes:protected] => Array ( [value] => 1+2 ) [attributes:protected] => Array ( [startLine] => 1 [endLine] => 1 ) ) [1] => PhpParser\Node\Scalar\String Object ( [subNodes:protected] => Array ( [value] => chongrui ) [attributes:protected] => Array ( [startLine] => 1 [endLine] => 1 ) ) ) ) [attributes:protected] => Array ( [startLine] => 1 [endLine] => 1 ) ))</span>
可以看到,這課AST只有一個節點Echo_,此節點有一個子節點exprs,可以使用$stmts[0]->exprs進行訪問。
對於節點中的attributes資訊是用來儲存startLine和endLine以及comments的。可以使用getAttributes(),getAttribute(‘startLine’),setAttribute(),hasAttribute()方法進行訪問。
開始行號startLine可以通過getLine()/setLine()方法進行訪問(也可以getAttribute(‘startLine’))。注釋資訊可以使用getDocComment()擷取。
訪問節點上的值:如訪問值“chongrui”,使用$stmts[0]->exprs[1]->value;即可。
4、節點遍曆
對抽象文法樹的遍曆非常方便,使用PhpParser\NodeTraverser類即可。同時,支援自訂的Visitor對象。因為在實際應用中,對PHP源碼進行分析,往往是不知道AST的具體結構,這時需要動態去判斷每個節點的類型資訊。
這些判斷統一寫到MyNodeVisitor中,該類繼承了一個父類NodeVisitorAbstract,這個類中有一些方法:
(1)beforeTraverse()方法用於遍曆之前,通常用來在遍曆前對值進行重設。
(2)afterTraverse()方法和(1)相同,唯一不同的地方是遍曆之後才觸發。
(3)enterNode()和leaveNode()方法在對每個節點訪問時觸發。
enterNode在進入節點時觸發,比如在訪問節點的子節點之前。這個方法可以返回NodeTraverser::DONT_TRAVERSER_CHILDREN,用來跳過該節點的孩子節點。
leaveNode在遍曆節點完成之後觸發。它可以返回
NodeTraverser::REMOVE_NODE,這種情況下,當前節點會被刪除。如果返回一個節點的集合,那麼這些節點會併入到父節點的array中,比如array(A,B,C),B節點被array(X,Y,Z)替換,變成array(A,X,Y,Z,C) .
下面的程式碼片段對$code進行解析,產生AST,並且在遍曆時,當發現遍曆節點時String類型時,就進行輸出。
結果會輸出1,2。
5、其他AST表示
有時候會將AST進行文本化持久儲存,這個功能PHP-Parser也支援。
(1)簡單的進行序列化
使用serialize()和unserialize()進行序列化和還原序列化操作,可以對AST進行持久儲存。
(2)易於閱讀的儲存形式
分別是完美列印和XML持久儲存,在這裡不做詳細介紹,有需要的時候可以看項目的文檔:
https://github.com/nikic/PHP-Parser/blob/master/doc/3_Other_node_tree_representations.markdown
6、總結
至少在PHP靜態分析方面,PHP-Parser在功能方面大大優於ANTLR。如何構建一個PHP自動化審計系統,這個PHP-Parser肯定會發揮不小的作用:)~