杀手级功能已出现在NoVerify静态分析器中:这是一种描述性的描述方式,不需要Go编程和代码编译。
为了引起您的兴趣,我将向您介绍一个简单但有用的检查的描述:
$x && $x;
此检查找到左右操作数相同的所有逻辑&&
表达式。
NoVerify是用Go编写的PHP静态分析器。 您可以在文章“ NoVerify:来自VKontakte团队的PHP Linter ”中阅读有关它的信息。 在这篇评论中,我将讨论新功能以及我们如何使用它。

背景知识
即使对于一个简单的新支票,您仍然需要在Go上编写几十行代码,您会开始怀疑:是否可能呢?
在Go上,我们编写了类型推断,lint的整个管道,元数据缓存以及许多其他重要元素,没有NoVerify,这些元素就无法实现。 这些组件是唯一的,但“禁止调用带有一组参数Y的函数X”之类的任务并非如此。 仅针对此类简单任务,就添加了动态规则机制。
动态规则使您可以将复杂的内部结构与解决典型问题分开。 定义文件可以单独存储和版本控制-可以由与NoVerify本身的开发无关的人员进行编辑。 每个规则都执行代码检查(有时称为验证)。
是的,如果我们使用一种描述这些规则的语言,则您总是可以编写一个语义上不正确的模板或忽略某些类型限制-这会导致误报。 但是,不会输入通过规则语言进行的数据竞争或对nil
指针的取消引用。
模板描述语言
描述语言在语法上与PHP兼容。 这简化了它的研究,也使使用相同的PhpStorm编辑规则文件成为可能。
在rules文件的最开始,建议插入一条指令来缓解您喜欢的IDE:
<?php
我的第一个关于语法和可能的模板过滤器的实验是phpgrep 。 它可能单独有用,但是在NoVerify内部,它变得更加有趣,因为现在它可以访问类型信息。
我的一些同事已经在工作中尝试了phpgrep,这是赞成选择这样一种语法的另一个论点。
Phpgrep本身是针对PHP的gogrep改编版(您可能对cgrep也可能感兴趣)。 使用此程序,您可以通过语法模板搜索代码。
一种替代方法是PhpStorm中的结构搜索和替换 (SSR)语法。 优点是显而易见的-这是一种现有格式,但是我在实现phpgrep之后才发现此功能。 您当然可以提供技术解释:语法与PHP不兼容, 我们的解析器将无法掌握它,但是这种令人信服的“真实”原因是在编写自行车后发现的。
实际上,还有另一种选择
可能需要以几乎一对一的方式显示带有PHP代码的模板,或者采用另一种方式:发明一种新语言,例如使用S-expressions的语法。
PHP-like Lisp-like ----------------------------- $x = $y | (expr = $x $y) fn($x, 1) | (expr call fn $x 1) : (or (expr == (type string (expr)) (expr)) (expr == (expr) (type string (expr))))
最后,我认为模板的可读性仍然很重要,我们可以通过phpdoc属性添加过滤器。
clang-query是类似想法的一个示例,但是它使用了更传统的语法。
我们创建并运行我们自己的诊断程序!
让我们尝试为分析器实施新的诊断。
为此,您需要安装NoVerify。 如果系统中没有Go-toolchain,请采用二进制发行版 (如果有,则可以从源代码编译所有文件)。
问题陈述
PHP有许多有趣的功能,其中之一是parse_str 。 她的签名:
如果您从文档中查看此示例,您将了解这里出了什么问题:
$str = "first=value&arr[]=foo+bar&arr[]=baz"; parse_str($str); echo $first;
嗯,来自字符串的参数在当前范围内。 为避免这种情况,我们将在新测试中要求使用函数的第二个参数$result
,以便将结果写入此数组。
创建自己的诊断
创建myrules.php
文件:
<?php parse_str($_);
通常,规则文件是顶层表达式的列表,每个表达式都被解释为phpgrep模板。 每个这样的模板都需要一个特殊的phpdoc注释。 只需要一个属性-一个带有警告文本的错误类别。
现在总共有四个级别: error
, warning
, info
甚至。 前两个是关键的:如果至少一个关键规则有效,则linter将在执行后返回非零代码。 在属性本身之后,如果模板被触发,短绒棉将发出警告文本。
我们编写的模板使用$_
这是一个未命名的模板变量。 我们可以称它为$x
,但是由于我们不对该变量做任何事情,因此可以给它一个“空”名称。 模板变量和PHP变量之间的区别在于,前者与任何表达式都完全一致,而不仅仅是“文字”变量。 这很方便:我们经常需要查找未知表达式,而不是特定变量。
开始新的诊断
创建一个小的调试文件test.php
:
<?php function f($x) { parse_str($x);
接下来,使用此文件的规则运行NoVerify:
$ noverify -rules myrules.php test.php
我们的警告将如下所示:
WARNING myrules.php:4: parse_str without second argument at test.php:4 parse_str($x); ^^^^^^^^^^^^^
默认检查的名称是规则文件的名称以及定义此检查的行。 在我们的例子中,这是myrules.php:4
。
您可以使用@name <name>
属性设置名称。
@Name示例
parse_str($_);
WARNING parseStrResult: parse_str without second argument at test.php:4 parse_str($x); ^^^^^^^^^^^^^
命名规则遵循其他诊断法则:
- 可以通过
-exclude-checks
禁用 - 可以通过
-critical
重新定义-critical
级别
处理类型
前面的示例对您好世界很有用-但通常我们需要知道表达式的类型以减少诊断操作的次数
例如,对于in_array函数,当第一个参数( $needle
)是字符串类型时,我们要求参数$strict=true
。
为此,我们有结果过滤器。
一种这样的过滤器是@type <type> <var>
。 它允许您丢弃不适合枚举类型的所有内容。
in_array($needle, $_);
在这里,我们将第一个参数的名称赋予in_array
调用,以将类型过滤器绑定到它。 仅当$needle
类型为string
时,才会发出警告。
过滤器集可以与@or
运算符结合使用:
$x == $y;
在上面的示例中,模式仅匹配那些==
表达式,其中任何操作数的类型均为string
。 可以假设没有@or
所有过滤器都通过@and
组合,但这无需明确指出。
限制诊断范围
对于每个测试,您可以指定@scope <name>
:
@scope all
默认值,验证无处不在;@scope root
仅在顶层启动;@scope local
仅在函数和方法内部运行。
假设我们要在函数主体外部报告return
值。 在PHP中,这有时是有道理的-例如,当从函数连接文件时...但是在本文中,我们对此予以谴责。
return $_;
让我们看看该规则的行为:
<?php function f() { return "OK"; } return "NOT OK";
同样,您可以请求使用*_once
而不是require
和include
:
require $_; include $_;
现在,在匹配模式时,并不会一直考虑括号。 模式(($x))
不会找到“括号中的所有表达式”,而只是忽略括号的任何表达式。 但是, $x+$y*$z
和($x+$y)*$z
按其应有的方式工作。 此功能来自使用令牌(
和)
的困难,但是有可能在下一个发行版中恢复该顺序。
分组模板
当模板上出现重复的docdoc注释时,可以组合模板。
一个简单的例子来演示:
现在,假设在以下示例中没有此功能的描述规则将是多么不愉快!
{ $x > $y; $x < $y; $x >= $y; $x <= $y; $x == $y; }
本文中指定的记录格式只是建议的选项之一。 如果您想参与选择,那么您将有这样的机会:您需要为那些比其他人更喜欢的报价提供+1。 有关更多详细信息, 请单击此处 。
动态规则如何整合

在启动时,NoVerify尝试查找rules
参数中指定的规则文件。
接下来,将此文件解析为常规的PHP脚本,并从生成的AST中收集一组绑定了phpgrep模板的规则对象。
然后,分析器根据通常的方案开始工作-唯一的区别是,对于某些已检查的代码段,它启动了一组绑定规则。 如果触发了规则,则会显示警告。
成功被认为是phpgrep模板与至少一个过滤器集(它们用@或分隔)通过的@or
。
在这个阶段,即使有很多动态规则,规则机制也不会显着降低Linter的运行速度。
匹配算法
使用天真的方法,对于每个AST节点,我们需要应用所有动态规则。 这是一个非常低效的实现,因为大部分工作都是徒劳的:许多模板都有一个特定的前缀,我们可以通过这些前缀对规则进行聚类。
这类似于并行匹配的想法,但是我们不会诚实地构建NFA,而只是“并行化”计算的第一步。
考虑具有三个规则的示例:
$_ ? $x : $x; explode("", ${"*"}); if ($_);
如果我们有N个元素和M条规则,那么采用幼稚的方法,我们将要执行N * M个操作。 从理论上讲,可以将这种复杂度降低为线性并得到O(N)
-如果将所有模式组合为一个并按其执行匹配,例如Go的regexp包。
但是,实际上,到目前为止,我一直专注于这种方法的部分实现。 它将允许将上面文件中的规则分为三类,并为没有规则对应的那些AST元素分配第四个空类别。 因此,每个元素最多执行一条规则。
如果我们有成千上万的规则,并且会感觉到速度明显下降,则算法将最终确定。 同时,解决方案的简单性和所带来的加速适合我。
当前语法复制@var
和@var
,但是我们可能需要新的运算符,例如,“ type is not equal”。 想象一下它的外观。
我们至少有两个重要优先事项:
- 注释的可读性和简洁性。
- IDE可以提供最大的支持,而无需额外的努力。
对于PhpStorm,有一个php-annotations插件,可添加自动完成功能,过渡到注释类以及使用phpdoc注释的其他实用性。
在实践中,优先级(2)意味着您所做的决策与IDE和插件的期望没有矛盾。 例如,您可以使用php-annotations插件可以识别的格式进行注释:
class Filter { public $value; public $type; public $text; }
然后将过滤器应用于类型将如下所示:
@Type($needle, eq=string) @Type($x, not_eq=Foo)
用户可以转到Filter
的定义,然后会提示他们一系列可能的参数(类型/文本/等)。
替代的录制方法,其中一些是同事建议的:
@type string $needle @type !Foo $x @type $needle == string @type $x != Foo @type(==) string $needle @type(!=) Foo $x @type($needle) == string @type($x) != Foo @filter type($needle) == string @filter type($x) != Foo
然后我们有点分心,忘了它全部都在phpdoc中,出现了:
(eq string (typeof $needle)) (neq Foo (typeof $x))
尽管还播放了带有postfix录制选项的声音。 用于描述类型和值约束的语言可以称为第六种:
@eval string $needle typeof = @eval Foo $x typeof <>
最佳选择的搜索仍未完成...
与Phan的可扩展性比较
作为Phan的优势之一,文章“ 使用PHPStan,Phan和Psalm的示例对PHP代码进行静态分析 ”表明了可扩展性。
这是在示例插件中实现的:
我们想评估我们的代码对PHP 7.3的准备程度(特别是确定是否具有不区分大小写的常量)。 我们几乎可以确定没有这样的常数,但是在12年内可能发生任何事情-应该对其进行检查。 并且我们为Phan编写了一个插件,如果在define()中使用了第三个参数,该插件将发誓。
这是插件代码的外观(格式针对宽度进行了优化):
<?php use Phan\AST\ContextNode; use Phan\CodeBase; use Phan\Language\Context; use Phan\Language\Element\Func; use Phan\PluginV2; use Phan\PluginV2\AnalyzeFunctionCallCapability; use ast\Node; class DefineThirdParamTrue extends PluginV2 implements AnalyzeFunctionCallCapability { public function getAnalyzeFunctionCallClosures(CodeBase $code_base) { $def = function(CodeBase $cb, Context $ctx, Func $fn, $args) { if (count($args) < 3) { return; } $this->emitIssue( $cb, $ctx, 'PhanDefineCaseInsensitiv', 'define with 3 arguments', [] ); }; return ['define' => $def]; } } return new DefineThirdParamTrue();
这是在NoVerify中可以完成的方法:
<?php define($_, $_, $_);
我们希望获得大致相同的结果-以便尽可能简单地完成琐碎的事情。
结论
链接,有用的材料
这里收集了重要的链接,本文中可能已经提到了一些重要的链接,但是为了清楚和方便起见,我将它们收集在一个地方。
如果您需要更多可以实施的规则示例,则可以浏览NoVerify测试 。