导读

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 token
echo token_name(260);        // -> "T_EVAL"// a token constant maps to its own name
echo 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序列。

<?php
$tokens  =  token_get_all ('<?php echo "Hello World!"; ?>');
print_r($tokens);
?>

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

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_TAG
T_ECHO
T_WHITESPACE
T_CONSTANT_ENCAPSED_STRING
T_WHITESPACE
T_CLOSE_TAG

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

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

使用PHP_CodeSniffer定制规则

<?php
# Check for valid contents.
if ($obj->contentsAreValid($array)) {$value = $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->getTokens();if ($tokens[$stackPtr]['content']{0} === '#') {$error = '禁止使用#号进行单行注释;扫描发现 %s';$data  = array(trim($tokens[$stackPtr]['content']));$phpcsFile->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对象,即当前正在被处理的代码文件对象;第二个是$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[$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/FireLine
D:/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_CodeSniffer 的代码静态分析原理相关推荐

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

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

  2. 软件测试 | 测试开发 | 年薪超过40W,一位测试媛宝妈的 BAT 大厂逆袭之旅

    本文为霍格沃兹测试学社优秀学员跳槽笔记,测试开发进阶学习文末加群. 本人之前是在一家二流互联网企业(已上市,不算真正的互联网),工作2年多.因为业务不断的调整和结婚生孩子,导致绩效不佳还是刚入职时的薪 ...

  3. 软件测试测试开发技能

    从事软件测试许多年,想必很多人都有感到迷茫不知所措的时候,人生的十字路口有很多,该如何抉择呢?有人成功转型,QA.项目管理.配置管理.当然还有技术型,性能测试.自动化测试.测试开发,而想要延续走技术型 ...

  4. 软件测试 | 测试开发 | 从外包菜鸟到测试开发,薪资一年翻三倍,连自己都不敢信!(附面试真题与答案)

    本人本科就读于某普通院校(很普通的那种技术示范学院),毕业后懵懂的加入了软件测试这个行业,至今有三年工作经验.通过在霍格沃兹测试学院的这段学习经历,我的职业角色完成了从功能测试到外包测试,再到测试开发 ...

  5. 软件测试 | 测试开发| 80后专科学历入职世界500强企业,二线城市年薪超30W

    本文为霍格沃兹测试学院优秀学员跳槽笔记,测试开发进阶学习文末加群. 这篇文章是写给想在测试行业或者转行做测试行业的小伙伴的.我将叙述如何从0到1转行到测试行业并入职世界500强企业.先交待一下我的背景 ...

  6. 软件测试 | 测试开发 | 做为测试,那些必须掌握的测试技术体系

    本文节选自霍格沃兹测试学院内部教材 软件测试技术是软件开发过程中的一个重要组成部分,是贯穿整个软件开发生命周期.对软件产品(包括阶段性产品)进行验证和确认的活动过程.其目的是尽快尽早地发现在软件产品中 ...

  7. 软件测试 | 测试开发 | 双非院校,从外包到外企涨薪85%,他的涨薪秘籍全公开

    本文为霍格沃兹测试开发学社优秀学员跳槽笔记,测试开发进阶学习文末加群. 本身是一所不入流的院校毕业的一名建工类专业的瓜娃子,至今记得当初是因为找工作被培训公司忽悠才加入到这个行业的,抱着做着试试的想法 ...

  8. 软件测试 | 测试开发 | 测试人生 | 00后0经验应届毕业生拿下2线城市15W offer,好励志~

    本文为霍格沃兹测试开发学社优秀学员跳槽笔记,测试开发进阶学习文末加群. 本人毕业于武汉市某不知名二本院校的物联网工程专业,物联网专业在我们学校是偏硬件的,对于软件的学习仅仅停留在基本的理论和操作方面. ...

  9. 软件测试/测试开发丨Java or Python?测试开发工程师如何选择合适的编程语言?

    很多测试开发工程师尤其是刚入行的同学对编程语言和技术栈选择问题特别关注,毕竟掌握一门编程语言要花不少时间成本,也直接关系到未来的面试和就业(不同企业/项目对技术栈要求也不一样),根据自身情况做一个相对 ...

最新文章

  1. PlayMaker GUI的Normalized
  2. Everyday a English
  3. LaTeX单栏和双栏设置
  4. MySQL高级 - SQL优化 - 子查询优化
  5. IOS原生地图与高德地图
  6. Java web后端6 java Bean EL表达式
  7. iOS强制切换横屏、竖屏
  8. 解决由于sz rz导致抓包时文件容量增加
  9. c++中判断某个值在字典的value中_Python元组、字典、集合的简单介绍
  10. loadrunner11的安装
  11. 在centos虚拟机中修改IP地址
  12. 微信小程序无法获取头像,昵称的解决办法 (原生)
  13. 路由及路由器工作原理深入解析3:路由与端口
  14. 全自动爬虫,你爱了么
  15. ilog开发培训大纲
  16. android 关闭jack_Android7.0编译 jack常见错误类型及解决办法
  17. 学习AI人工智能,你必须要知道的4件事!
  18. 修改功能测试的测试点
  19. python如何保存训练好的模型_Python机器学习7:如何保存、加载训练好的机器学习模型...
  20. 【Python学习笔记】1:黑洞数和啤酒问题

热门文章

  1. securecrt连接不上vmware
  2. Eclipse的架构
  3. spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during v
  4. 高中计算机期末试题,高中信息技术期末试卷
  5. DTS中video-interfaces接口详解
  6. MATLAB音频信号处理(一):函数简易用法(audioread,sound函数)
  7. 高斯消去法解线性方程组的fortran程序实现
  8. 2021年团体程序设计天梯赛-总决赛题解
  9. 告别2022,重新出发
  10. 嵌入式硬件电路设计的基本技巧