PHP的语法分析器PHP-Parser
我们都知道PHP脚本的执行过程,先是由Zend引擎将PHP源码编译为opcode序列,再由Zend VM去解释执行。一般编译的过程都是先进行词法分析、语法分析,然后才是编译。在经过语
有关于抽象语法树 我们都知道PHP脚本的执行过程,先是由Zend引擎将PHP源码编译为opcode序列,再由Zend VM去解释执行。一般编译的过程都是先进行词法分析、语法分析,然后才是编译。在经过语法分析之后,有一个抽象语法树(Abstract Syntax Tree或者缩写为AST)的概念,他算是语法分析的产出,之后的编译过程是编译器在AST基础上进行的。 但是PHP比较特殊,Zend引擎在语法分析之后直接产出了opcode,没有生成AST。这样做最大的好处是加快了编译过程,坏处则是丧失了一些自由性,难以优化以及导致编译程序逻辑复杂。具体PHP官网上有一篇wiki探讨了在PHP的编译过程中引入AST,当然实现起来难度肯定很大。 我们在平时一般不会接触到PHP的编译过程,但是AST却是个有用的东西,我们平时工作中很多工具里面都有AST,比如PHP_CodeSniffer、PHP_Depend、ZendStudio、PDT……需要对源码进行分析的场合,或多或少都需要AST的帮忙。 PHP的词法分析 说到词法分析和语法分析,必须提到的就是lex和yacc,计算机科班出身的在编译原理课上肯定都学过,不过反正我是早忘的差不多了。现在的词法分析器、语法分析器基本上都是用这两个工具生成的。 PHP的词法分析,我们可以直接借助于Zend引擎。PHP有一个tokenizer扩展,作用就是对PHP代码进行词法分析并生成词法分析后的token(单词)。词法分析只是将源代码解析生一堆token的序列,并不处理token和token之间的关系,那是语法分析的工作。 PHP的语法分析 PHP的语法分析的工具其实有很多,我列出几个: 对于一个程序员来说,使用熟悉的语言做事情是最爽的,PHP-Parser是PHP语言实现,虽然运行起来会比较慢,但在上面做开发会比较容易。我调研过pfff,OCaml在执行性能上虽然可以与C相当,但是OCaml作为函数式编程语言,它的语法实在是比较难懂,如果你不介意学习一门新语言并且持续在之上投入精力,可以考虑一下pfff。 PHP-Parser 上面提到,PHP-Parser是一个PHP写的语法分析器,它的基本功能就是生成抽象语法树,除此之外附带了一些其他功能,包括:语法树的序列化/反序列化、语法树构建、代码生成,实现的都不复杂。PHP-Parser的项目主页是: 安装: 安装的方法有很多,这里不一一赘述,具体可参考: 我是使用的composer安装,只需在项目代码的composer.json中加入: { "require": { "nikic/php-parser": "0.9.3" } } 然后执行下面命令即可: php composer.phar install 引入Autoloader的时候,如果你的PHP是5.3以上,无需像文档中说的引入它的bootstrap.php,只要加载composer的autoloader即可。如果php版本低于5.3,还是老老实实加载他的bootstrap.php,里面定义了一个php5.3才引入的函数lcfirst()。 简单范例: 官方文档中给出的范例已经非常直观,这里也不做赘述了,除非哪天兴致来了再补充这章。 代码结构: PHP-Parser主要包含以下几个功能块: Lexer: 调用tokenizer的token_get_all()完成词法分析,token_get_all()返回的是token数组,Lexer将数组中的每个token映射到PHP-Parser的Node类当中。 如果你要在低版本的PHP环境中分析高版本PHP的代码,那么Lexer中有一个PHPParser_Lexer_Emulative类PHP语法,用来模拟生成高版本PHP中才有的token。 Lexer包含下面两个关键函数: public function startLexing($code) {} public function getNextToken(&$value = null, &$startAttributes = null, &$endAttributes = null) {} 其中startLexing接受PHP代码作为参数,如果你想接收一个文件名作为参数,可以派生此类,并重写startLexing方法,以下是官方文档的一个例子: public function startLexing($fileName) { if (!file_exists($fileName)) { throw new InvalidArgumentException(sprintf('File "%s" does not exist', $fileName)); } parent::startLexing(file_get_contents($fileName)); } getNextToken则允许你在处理下一个token时,添加或者删除一些额外信息,例如下面这个同样是官方文档中的例子: public function getNextToken(&$value = null, &$startAttributes = null, &$endAttributes = null) { $tokenId = parent::getNextToken($value, $startAttributes, $endAttributes); // only keep startLine attribute unset($startAttributes['comments']); unset($endAttributes['endLine']); $endAttributes['fileName'] = $fileName; return $tokenId; } 要注意上面代码里参数都是引用传递的。 Parser: 经过词法分析之后,就生成了一大堆的token,token经过Parser的解析,才会生成抽象语法树。PHPParser_Parser类是自动生成的,没事千万不要去改它。 PHPParser_Parser的构造函数接受一个PHPParser_Lexer类型作为参数,也就是上面的词法分析器。 Node: 经过Parser的解析,生成了抽象语法树,抽象语法树的每一个节点都是一个Node对象。PHP一共有140+种不同的Node对象,PHPParser将他们分成了3类: PHPParser_Node_Stmt:声明,即没有返回值并且不能出现在表达式中的代码。例如函数定义、类定义以及控制语句等。PHPParser_Node_Expr:表达式,即有返回值并且可以出现在表达式中的语句,例如单个变量、函数调用等PHPParser_Node_Scalar:标量,例如字符串、整型、预定义常量如__FILE__等还有一些不属于上述三种类型的,例如PHPParser_Node_Name、PHPParser_Node_Arg 所有Node的基类PHPParser_NodeAbstract实现了IteratorAggregate接口,这意味着你可以直接foreach来对其进行迭代访问其子节点。 每一个Node都实现了一些方法,以方便对其进行访问,例如PHPParser_Node_Stmt_Class也就是类定义,有以下几个方法可以使用: public function isAbstract() {} public function isFinal() {} public function getMethods() {} PHPParser_Node_Stmt_ClassMethod则有: public function isPublic() {} public function isProtected() {} public function isPrivate() {} public function isAbstract() {} public function isFinal() {} public function isStatic() {} Traverser: 由Parser生成的抽象语法树,可以通过Traverser进行遍历,并在遍历过程中通过Visitor进行操作,比如下面这个官方的例子(我做了中文注释): <?php $code = "<?php // some code"; $parser = new PHPParser_Parser(new PHPParser_Lexer()); $traverser = new PHPParser_NodeTraverser(); //创建一个Traverser $prettyPrinter = new PHPParser_PrettyPrinter_Default(); //向Traverser添加Visitor,真正操作代码的是Visitor $traverser->addVisitor(new MyNodeVisitor()); try { // 解析代码,生成抽象语法树$stmts $stmts = $parser->parse($code); // 遍历代码,这时候Traverser回到调用Visitor对语法树进行操作 $stmts = $traverser->traverse($stmts); // 重新生成修改后的代码 $code = '<?php ' . $prettyPrinter->prettyPrint($stmts); echo $code; } catch (PHPParser_Error $e) { echo 'Parse Error: ', $e->getMessage(); } class MyNodeVisitor extends PHPParser_NodeVisitorAbstract { public function leaveNode(PHPParser_Node $node) { if ($node instanceof PHPParser_Node_Scalar_String) { $node->value = 'foo'; } } } 可以为Traverser添加多个Visitor,在遍历语法树的时候Traverser会依次调用每一个Visitor。 Traverser遍历语法树遵循以下步骤: 遍历开始前,依次调用每一个Visitor的beforeTraverse()方法,传入参数为当前抽象语法树访问抽象语法树中的第一个节点,在开始递归前依次调用每一个Visitor的enterNode()方法,传入参数为当前节点。若当前节点存在子节点,则递归依次处理其子节点,重复步骤2。若当前节点不存在子节点,则依次调用每一个Visitor的leaveNode()方法,传入参数为当前节点。所有的节点都遍历结束,则依次调用每一个Visitor的afterTraverse方法,传入参数为操作后的抽象语法树。 可以看出,要处理或者分析代码,主要是通过Visitor进行,Traverser只是适时的调用Visitor的指定方法而已。 Visitor: Visitor刚才已经提到, 其抽象基类就包含以下几个方法: public function beforeTraverse(array $nodes) { } public function enterNode(PHPParser_Node $node) { } public function leaveNode(PHPParser_Node $node) { } public function afterTraverse(array $nodes) { } 方法的作用就不重复讲了,需要注意的是这四个方法的返回值: Pretty printer: 一句话解释:将抽象语法树重新输出成格式化好的代码。上面Traverser的例子中有Pretty printer的用法。Pretty printer的功能还比较简单,目前只有Zend风格的,有需要可以自定制。 Serialize/UnSerialize: 将抽象语法树进行序列化/反序列化,序列化/反序列化的意义在于: 你可以将抽象语法树序列化后进行持久化存储,需要时读出来反序列化重新在内存中生成抽象语法树你可以将抽象语法树转换格式,以供其他程序读取。 目前PHP-Parser实现了XML格式的序列化/反序列化,官方的这篇文档有介绍。你也可以简单的调用php的serialize和unserialize来序列化/反序列化。 Builder: Builder的作用是创建抽象语法树,和Pretty print结合的话,可用于Code Generation。你可以根据一个抽象描述来生成PHP代码。这个功能很不错,会有很多应用场景: 具体如何使用,官方也有相应文档 What PHP-Parser Can’t Do? 知道PHP-Parser能做什么,我们还得知道它不能做什么。我们知道PHP是弱类型、解释型的语言,这意味着某些变量的类型,只有在运行时才能确定,例如如下代码: function foo($objPrint) { $objPrint->say("Hello World"); } 生成的抽象语法树如下: array(1) { [0]=> object(PHPParser_Node_Stmt_Function)#17 (2) { ["subNodes":protected]=> array(4) { ["byRef"]=> bool(false) ["params"]=> array(1) { [0]=> object(PHPParser_Node_Param)#12 (2) { ["subNodes":protected]=> array(4) { ["name"]=> string(8) "objPrint" ["default"]=> NULL ["type"]=> NULL ["byRef"]=> bool(false) } ["attributes":protected]=> array(2) { ["startLine"]=> int(2) ["endLine"]=> int(2) } } } ["stmts"]=> array(1) { [0]=> object(PHPParser_Node_Expr_MethodCall)#16 (2) { ["subNodes":protected]=> array(3) { ["var"]=> object(PHPParser_Node_Expr_Variable)#13 (2) { ["subNodes":protected]=> array(1) { ["name"]=> string(8) "objPrint" } ["attributes":protected]=> array(2) { ["startLine"]=> int(3) ["endLine"]=> int(3) } } ["name"]=> string(3) "say" ["args"]=> array(1) { [0]=> object(PHPParser_Node_Arg)#15 (2) { ["subNodes":protected]=> array(2) { ["value"]=> object(PHPParser_Node_Scalar_String)#14 (2) { ["subNodes":protected]=> array(1) { ["value"]=> string(11) "Hello World" } ["attributes":protected]=> array(2) { ["startLine"]=> int(3) ["endLine"]=> int(3) } } ["byRef"]=> bool(false) } ["attributes":protected]=> array(2) { ["startLine"]=> int(3) ["endLine"]=> int(3) } } } } ["attributes":protected]=> array(2) { ["startLine"]=> int(3) ["endLine"]=> int(3) } } } ["name"]=> string(3) "foo" } ["attributes":protected]=> array(2) { ["startLine"]=> int(2) ["endLine"]=> int(4) } } } 可以看出在PHPParser_Node_Expr_MethodCall当中,objPrint的类型是PHPParser_Node_Expr_Variable,并且没有任何关于变量的类型信息,因为这里并不知道objPrint的类型,只知道它是一个变量。 如果我们把代码改为下面这个形式: function foo(Print_Type $objPrint) { $objPrint->say("Hello World"); } 也就是在代码中加入Type Hint,那么我们可以通过Visitor在进入MethodCall这个Node时获取到参数的类型,并记录到当前Method的变量类型映射表中,在调用$objPrint的时候,就可以分析出objPrint对应的类型了。 不过,还有另外一种形式的问题,例如下面这个代码: function foo($name){ $classname = 'Class_'.$name; $obj = new $classname(); $obj->foo(); } 这种情况就比较悲催了,得知道$name的值才能知道$obj的类型,而$name的值只有运行时才会知道,目前我也没有想到有什么好的解决办法。 总结: 以上是调研PHP代码工具过程中,对于PHP-Parser的一些记录。通过PHP-Parser,我们可以构建自己的代码工具。初步的一个想法是先利用PHP-Parser和SQLite对PHP代码进行解析和创建索引。之后可以通过SQL来对抽象代码树进行修改,最终再生成PHP代码,实现对代码进行重构的目的。这种实现的好处是SQL大家都比较熟悉,相对比较灵活。 (编辑:武汉站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |