PHP-Parser 学习笔记

缘起

最近工作上接到一个任务,要把项目代码的控制器中所有没有判断过权限的public方法找出来。我们的控制器里有一个统一的”checkAllow”方法来进行权限判断。所以任务可以归结为找到所有代码里没有调用过这个方法的public方法及其所在的类。

有了这个思路,我首先想到的就是用php中自带的”token_get_all”方法获取到代码的分词,然后进行分析。然而这个函数太底层了,什么时候进入方法,什么时候退出方法等细节都需要繁琐的判断,而且还有标点、括号、空格之类无用信息的干扰。比如最简单的”<?php echo ‘Hi’, ‘World’;”分词结果如下:

看着就头疼了,更别说真实的项目中一个文件会有成百上千行,那分析出来的数组元素得以万为单位计数。

显然,通过自带的方法完成这个任务不太现实。于是,我开始搜索一些第三方的用于代码静态分析的库,最终找到了nikic大神写的PHP-Parser这个库。

原理

本质上,这个库也是调用”token_get_all”方法来获取代码分词的,但是在此基础之上,生成了”抽象语法树(Abstract Syntax Tree)”。比如”<?php echo ‘Hi’, ‘World’;”代码生成的语法树就是这个样子:

即使还没有开始学习如何使用这个库的api,看到这样清晰的结构,心理负担也比面对直接使用”token_get_all”得到的结果轻松不少。

入门

其实看到语法树的结构,我们的思路就比较清晰了,无非就是遍历找到所有的public方法,再看这个方法的代码里有没有调用过”checkAllow”方法。所以接下来的思路就是看看这个库到底给我们提供了哪些高端大气的方法去遍历这个树。

我们以一个最简单的接近真实项目文件的例子进行探索。

相信所有人最先有的冲动就是看看这段代码生成的语法树到底长啥样,我们就一起来看看吧。

“composer require nikic/php-parser”安装,之后”require autoload”文件这些基本的东西不就用多说了吧?

我们先来打印一下解析出的语法树:

结果如下:

看到”Stmt_Namespace”、”Stmt_Class”、”Stmt_ClassMethod”之类的命名,即使不继续看文档,我们也可以清晰地了解每个节点是干什么的。每个节点是一个对象,类名表示自己的类型,name属性表示自己的名字,stmts属性表示包含的子节点。

PHP中大约有140种不同类型的节点,在库中有各自对应的类。但是这里我们只关心例子中涉及到的这几个节点类型。

了解到这些,我们其实就可以开始搞事情了。

打印结果如下:

前面的例子中之所以包含了”__construct”方法,是因为有些类在构造方法中统一检查了读权限,在需要写的public方法中检查写权限。

分析到了方法这一层,接下来就要分析具体的代码语句了。

打印结果如下:

至此,我们甚至不需要继续深入研究这个库,就已经可以完成我的任务目标了。

进阶

就算任务可以完成了,也不能刚入门就调头走了呀。所以我们还要继续看看这个库有什么高端大气的功能。

解析出语法树,自然而然就要遍历这个语法树。之前的例子中,我们是以遍历数组的方式进行的,并且我们在遍历之前其实是知道代码结构的。在更广泛的应用当中,我们可能根本不知道代码结构是什么样子。接下来我们看看库里提供的更具抽象性的遍历方法。

嗯,就这么简单,我们遍历了所有的节点。然而并没有什么用,我们什么也没做。这是因为只有遍历器是不够的,我们还需要为遍历器添加一个或多个”访问者(NodeVisitor)”。

根据我们的任务,整理一下思路。我们主要的工作应该都在”enterNode”方法里,在这里需要根据节点的类型进行不同的操作,如果是public方法我们就继续判断代码中有没有检查权限的方法调用。如果不是public方法我们就可以直接跳过遍历。而像命名空间之类无关的节点我们也不再需要关心。

打印结果如下:

至此,我们用高端大气的遍历器完成了之前需要手写循环的任务。对比一下两种实现方式,可以发现各有优缺点。手写循环虽然繁琐,但是我们可以筛选自己关心的节点,跳过无关节点。而通过遍历器的话,就会一鼓脑地遍历所有节点。所以在enterNode里就要有不少类型的判断。

有没有更简便的方法呢?哈哈,我都感觉到麻烦了,nikic大神能想不到吗?是时候亮出另一个武器了–“NodeFinder”。

是不是感觉比之前用遍历器省事了很多?哈哈,其实本质上”NodeFinder”也是利用了遍历器。我们完全可以自己实现这个功能,无非就是在”enterNode”中判断一下节点类型,放到数组里,遍历完之后返回。

后续

一篇文章肯定无法介绍整个库的方方面面,PHP-Parser还有很多高级的特性,比如命名解析、代码合适化、常量解析、JSON语法树,甚至可以用程序动态生成代码等等。

这些高级的特性我暂时没有适用场景,就先不去深入了。如果你感兴趣的话,直接去啃官方文档吧。

参考

官方文档