写点什么

探究 PHP_CodeSniffer 的代码静态分析原理

  • 2022 年 9 月 26 日
    北京
  • 本文字数:4551 字

    阅读完需:约 15 分钟

导读


PHP_CodeSniffer 是一个用来检查 PHP 代码规范的开源项目。它主要通过词法分析的方式将 PHP 源码解析成 TOKEN 数组,然后在 TOKEN 中标记出不符合代码规范的代码位置。


目前编程语言可以分为两大类:


第一类是像 C/C++, .NET, Java 之类的编译型语言, 它们的共性是: 运行之前必须对源代码进行编译,然后运行编译后的目标文件。


以 Java 举例,第一类编译的过程都是先进行词法分析、语法分析,然后才是编译。在经过语法分析之后,有一个抽象语法树(AST)的概念,算是语法分析的产出,之后的编译过程是编译器在 AST 基础上进行的。


第二类比如:PHP, Javascript, Ruby, Python 这些解释型语言, 他们都无需经过编译即可"运行",虽然可以理解为直接运行,但它们并不是真的直接就被能被机器理解, 机器只能理解机器语言,那这些语言是怎么被执行的呢, 一般这些语言都需要一个解释器, 由解释器来执行这些源码, 实际上这些语言还是会经过编译环节, 只不过它们一般会在运行的时候实时进行编译。


以 PHP 举例,PHP 的运行过程是怎样的呢?


1.传递给 php 程序需要执行的文件, php 程序完成基本的准备工作后启动 PHP 及 Zend 引擎, 加载注册的扩展模块。


2.初始化完成后读取脚本文件,Zend 引擎对脚本文件进行词法分析,语法分析。然后编译成 opcode 执行。如果安装了 apc 之类的 opcode 缓存, 编译环节可能会被跳过而直接从缓存中读取 opcode 执行。


TOKEN


在文章开头,我们已经知道 PHP_CodeSniffer 的主要原理是将 PHP 源码解析成 TOKEN 数组,同时在 PHP_CodeSniffer 源码中,很多操作的核心就围绕着 TOKEN 展开。


那么问题来了,TOKEN 是什么?


PHP 词法解析器在解析 PHP 语言的过程中,PHP 语言的不同部分在内部被表示为类似 T_XXX 的类型,这个 T_XXX 的类型就叫 TOKEN,也叫标识符。


标识符部分列表:


代号语法参考|------PHP 官网共 119 个标识符,查看 PHP 官网标识符列表


在 PHP 中提供了 token_get_all(string $source)方法解析提供的 source 源码字符,然后使用 Zend 引擎的词法分析器获取源码中的 PHP 语言的 TOKEN 代号,就是上文中的 T_XXX。


TOKEN 代号都有对应的唯一值,比如 T_ABSTRACT 对应的是 312,是之前定义好的。


你也可以使用 PHP 自带的 token_name(312)方法获取 TOKEN 代号,示例代码如下:


<?php// 260 is the token value for the T_EVAL tokenecho token_name(260); // -> "T_EVAL"


// a token constant maps to its own nameecho token_name(T_FUNCTION); // -> "T_FUNCTION"?>


PHP 词法分析


在了解了 TOKEN 之后,就得详细了解一下词法分析了。词法分析就是从输入流里边一个字符一个字符的扫描,识别出对应的词素,最后把源文件转换成为一个 TOKEN 序列。


以最简单的一行代码举例词法分析的流程:


<?php echo "Hello World!"; ?>


是以 yy 开头命名,常用到的就是:yy_state, yy_text, yyleng, yy_cursor, yy_limit。


扫描 echo 前:


通过一个字符一个字符的扫描最终会得到一个 TOKEN 序列。


<?phptokens);?>


运行该文件后,输出结果:


Array ([0] => Array ( [0] => 376 [1] => 1 )[1] => Array ( [0] => 319 [1] => echo [2] => 1 )[2] => Array ( [0] => 379 [1] => [2] => 1 )[3] => Array ( [0] => 318 [1] => "Hello World!" [2] => 1 )[4] => ;[5] => Array ( [0] => 379 [1] => [2] => 1 )[6] => Array ( [0] => 378 [1] => ?> [2] => 1 ))


echo token_name(376);echo PHP_EOL;echo token_name(319);echo PHP_EOL;echo token_name(379);echo PHP_EOL;echo token_name(318);echo PHP_EOL;echo token_name(379);echo PHP_EOL;echo token_name(378);


输出为:


T_OPEN_TAGT_ECHOT_WHITESPACET_CONSTANT_ENCAPSED_STRINGT_WHITESPACET_CLOSE_TAG


结合这个 TOKEN 代号对应的语法以及 TOKEN 序列的输出,我们就能理解刚才那行代码词法分析后的 TOKEN 序列内容,并且能根据序列内容还原源代码。


所以,只要有词法分析后的 TOKEN 序列内容,我们就可以结合我们自己的需求,在 TOKEN 序列内容的基础上进行 PHP_CodeSniffer 规则的定制。


使用 PHP_CodeSniffer 定制规则


<?php

Check for valid contents.

if (array)) {obj->getValue();# Value needs to be an array.if (is_array(value) === false) {# Error.obj->throwError();exit();}}?>


1.规则库目录介绍


首先 PHP_CodeSniffer 的所有规则都存放在/src/Standards/目录下,默认该目录下已经有 Generic、PEAR、PSR1、PSR2、PSR12、Squiz、Zend 等目录,每一个目录其实就是一个规则库。如果想使用其中某一个规则库,例如 PEAR 规则库,运行时加入参数–standard=D:/git/PHP_CodeSniffer/src/Standards/PEAR,扫描时就会使用该规则库进行扫描。


2.创建新规则库目录


<?xml version="1.0"?><ruleset name="FireLine"><description>360 FireLine rule for test.</description></ruleset>


里面定义了规则库的名称和描述。


3.创建规则实现文件


然后在 Sniffs 文件夹中新建 Commenting 文件夹,代表了一个更细的注解分类,接着这个文件夹里面新建 php 文件 DisallowHashCommentsSniff.php(每个规则实现文件对应一个 Sniff 结尾的 php 文件),规则实现的内容如下:


<?php/**


  • This sniff prohibits the use of Perl style hash comments.

  • PHP version 5

  • @category PHP

  • @package PHP_CodeSniffer

  • @author Your Name <you@domain.net>

  • @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence

  • @link http://pear.php.net/package/PHP_CodeSniffer*/


namespace PHP_CodeSniffer\Standards\FireLine\Sniffs\Commenting;


use PHP_CodeSniffer\Sniffs\Sniff;use PHP_CodeSniffer\Files\File;


class DisallowHashCommentsSniff implements Sniff{/*** Returns the token types that this sniff is interested in.** @return array(int)*/public function register(){return array(T_COMMENT);}//end register()


/** * Processes this sniff, when one of its tokens is encountered. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being checked. * @param int                         $stackPtr  The position of the current token in the *                                               stack passed in $tokens. * * @return void */public function process(File $phpcsFile, $stackPtr){    $tokens = $phpcsFile-&gt;getTokens();    if ($tokens[$stackPtr]['content']{0} === '#') {        $error = '禁止使用#号进行单行注释;扫描发现 %s';        $data  = array(trim($tokens[$stackPtr]['content']));        $phpcsFile-&gt;addError($error, $stackPtr, 'Found', $data);    }}//end process()
复制代码


}//end class?>


4.规则实现详解


首先每个 sniff 类必须实现 Sniff 接口,该接口内有两个两个必须要实现的方法:register()和 process()方法。


首先通过调用 register()方法告诉 PHP_CodeSniffer 我们要检查编码标准哪些方面(也就是我们要查找哪些类型的 TOKEN)。然后当词法解析引擎碰到这些 TOKEN 时就会调用 process()方法来做进一步处理。


在该文件中,我们可以看到 register()方法中是想查找 T_COMMENT 类型的 TOKEN,通过 PHP 官网提供的 TOKEN 列表中得到 T_COMMENT 对应的 PHP 语法为 // 或 #,以及 PHP5 下的 /* */,即 PHP 语法中的单行注释。所以说,当词法解析引擎遇到单行注释类别的 TOKEN 时,就会自动继续调用 process()方法。


我们接着来看 process()方法,该方法有两个参数,第一个是 phpcsFile 对象,即当前正在被处理的代码文件对象;第二个是 phpcsFile 对象,即当前正在被处理的代码文件对象;第二个是 stackPtr 参数,这个参数的意思是我们当前关注的 TOKEN-即代表着单行注释的 TOKEN(T_COMMENT)在 TOKEN 序列中的索引。这里正好回应了上面提到的 PHP 词法分析原理,将 PHP 源文件解析成一个 TOKEN 序列,而 $stackPtr 参数表示当前 TOKEN 在这个 TOKEN 序列的索引位置。


接下来是 process()方法内的实现,首先通过 PHP_CodeSniffer 封装的 getTokens()方法来获得当前文件的 TOKEN 序列。在通过索引获取到我关注的 T_COMMENT 对应的 TOKEN 后,进一步获取 TOKEN 数组里面的 content 索引对应的内容。


TOKEN 数组里面包含了 code、type、content 这三种索引,分别对应的内容是 TOKEN 代号唯一值、TOKEN 代号即 T_COMMENT、TOKEN 所对应的代码。所以判断条件里面的 tokens[tokens[stackPtr][‘content’]{0} 的意思是取 TOKEN 序列中我们所关注的 T_COMMENT 对应的 TOKEN,然后取这个 TOKEN 中对应的代码中的第一个字符。如果这个字符是 #,说明触发了单行注释禁止使用了 # 号的规则。我们最后通过 addError()方法来记录触发规则的 TOKEN 和对应的码,以及我们的规则解释。


5.规则运行


php D:/git/PHP_CodeSniffer/bin/phpcs--standard=D:/git/PHP_CodeSniffer/src/Standards/FireLineD:/git/PHP_CodeSniffer/src/Standards/FireLine/Tests--report=xml --report-file=E:/RedlineReport/php_report01.xml


D:/git/PHP_CodeSniffer/src/Standards/FireLine/Tests 目录中存放了有问题的测试代码文件。


<?xml version="1.0" encoding="UTF-8"?><phpcs version="3.3.1">xml version="1.0" encoding="UTF-8"?><file name="D:\git\PHP_CodeSniffer\src\Standards\FireLine\Tests\Commenting\test01.php" errors="3" warnings="0" fixable="0"><error line="3" column="1" source="FireLine.Commenting.DisallowHashComments.Found" severity="5" fixable="0">禁止使用 #号进行注释;扫描发现 # Check for valid contents.</error><error line="7" column="5" source="FireLine.Commenting.DisallowHashComments.Found" severity="5" fixable="0">禁止使用 #号进行注释;扫描发现 # Value needs to be an array.</error><error line="9" column="9" source="FireLine.Commenting.DisallowHashComments.Found" severity="5" fixable="0">禁止使用 #号进行注释;扫描发现 # Error.</error></file></phpcs>


从报告中可以看到,之前准备测试代码文件中的三处错误,都能成功检查出来。


总结


从 PHP_CodeSniffer 的规则库可以看出大部分都是检查代码的风格类问题,这也是基于词法分析做静态分析所带来的局限性。相比较 PMD、SonarQube 等使用语法分析(基于结构化的语法信息树)的开源项目,无法检查出更多复杂的代码场景问题。同时由于检查的代码场景较为简单,基于模式匹配的代码自动修复功能更容易实现,准确度也较高。PHP_CodeSniffer 项目中开启 PHP Code Beautifier and Fixer (phpcbf)功能,就可以实现代码自动修复功能,并支持生成修复前后的 diff 报告,算是 PHP_CodeSniffer 项目的一大亮点。


参考文献


**[深入理解 PHP 内核] **


http://www.php-internals.com/book/?p=chapt01/01-02-code-structure


**[PHP-Zend 引擎剖析] **


https://blog.csdn.net/raphealguo/article/details/16941531


[Coding Standard Tutorial]


https://github.com/squizlabs/PHP_CodeSniffer/wiki/Coding-Standard-Tutorial


点击下方链接免费领取:性能测试+接口测试+自动化测试+测试开发+测试用例+简历模板+测试文档

http://qrcode.testing-studio.com/f?from=infoQ&url=https://ceshiren.com/t/topic/22265

用户头像

社区:ceshiren.com 微信:ceshiren2021 2019.10.23 加入

微信公众号:霍格沃兹测试开发 提供性能测试、自动化测试、测试开发等资料,实时更新一线互联网大厂测试岗位内推需求,共享测试行业动态及资讯,更可零距离接触众多业内大佬。

评论

发布
暂无评论
探究 PHP_CodeSniffer 的代码静态分析原理_霍格沃兹测试开发学社_InfoQ写作社区