如何在不编写任何Go代码的情况下将检查添加到NoVerify

杀手级功能已出现在NoVerify静态分析器中:这是一种描述性的描述方式,不需要Go编程和代码编译。


为了引起您的兴趣,我将向您介绍一个简单但有用的检查的描述:


/** @warning duplicated sub-expressions inside boolean expression */ $x && $x; 

此检查找到左右操作数相同的所有逻辑&&表达式。


NoVerify是用Go编写的PHP静态分析器。 您可以在文章“ NoVerify:来自VKontakte团队的PHP Linter ”中阅读有关它的信息。 在这篇评论中,我将讨论新功能以及我们如何使用它。



背景知识


即使对于一个简单的新支票,您仍然需要在Go上编写几十行代码,您会开始怀疑:是否可能呢?


在Go上,我们编写了类型推断,lint的整个管道,元数据缓存以及许多其他重要元素,没有NoVerify,这些元素就无法实现。 这些组件是唯一的,但“禁止调用带有一组参数Y的函数X”之类的任务并非如此。 仅针对此类简单任务,就添加了动态规则机制。


动态规则使您可以将复杂的内部结构与解决典型问题分开。 定义文件可以单独存储和版本控制-可以由与NoVerify本身的开发无关的人员进行编辑。 每个规则都执行代码检查(有时称为验证)。


是的,如果我们使用一种描述这些规则的语言,则您总是可以编写一个语义上不正确的模板或忽略某些类型限制-这会导致误报。 但是,不会输入通过规则语言进行的数据竞争或对nil指针的取消引用。


模板描述语言


描述语言在语法上与PHP兼容。 这简化了它的研究,也使使用相同的PhpStorm编辑规则文件成为可能。


在rules文件的最开始,建议插入一条指令来缓解您喜欢的IDE:


 <?php /** *      , *        PHP-. * * @noinspection ALL */ // ...  —   . 

我的第一个关于语法和可能的模板过滤器的实验是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,请采用二进制发行版 (如果有,则可以从源代码编译所有文件)。


如果您没有安装NoVerify,则可以继续阅读,但是假装重现列出的步骤并欣赏结果!

问题陈述


PHP有许多有趣的功能,其中之一是parse_str 。 她的签名:


 //   encoded_string,     //   URL,      //   (  ,    result). parse_str ( string $encoded_string [, array &$result ] ) : void 

如果您从文档中查看此示例,您将了解这里出了什么问题:


 $str = "first=value&arr[]=foo+bar&arr[]=baz"; parse_str($str); echo $first; // value echo $arr[0]; // foo bar echo $arr[1]; // baz 

嗯,来自字符串的参数在当前范围内。 为避免这种情况,我们将在新测试中要求使用函数的第二个参数$result ,以便将结果写入此数组。


创建自己的诊断


创建myrules.php文件:


 <?php /** @warning parse_str without second argument */ parse_str($_); 

通常,规则文件是顶层表达式的列表,每个表达式都被解释为phpgrep模板。 每个这样的模板都需要一个特殊的phpdoc注释。 只需要一个属性-一个带有警告文本的错误类别。


现在总共有四个级别: errorwarninginfo甚至。 前两个是关键的:如果至少一个关键规则有效,则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示例


 /** * @name parseStrResult * @warning parse_str without second argument */ 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> 。 它允许您丢弃不适合枚举类型的所有内容。


 /** * @warning 3rd arg of in_array must be true when comparing strings * @type string $needle */ in_array($needle, $_); 

在这里,我们将第一个参数的名称赋予in_array调用,以将类型过滤器绑定到它。 仅当$needle类型为string时,才会发出警告。


过滤器集可以与@or运算符结合使用:


 /** *     -. * * @warning strings must be compared using '===' operator * @type string $x * @or * @type string $y */ $x == $y; 

在上面的示例中,模式仅匹配那些==表达式,其中任何操作数的类型均为string 。 可以假设没有@or所有过滤器都通过@and组合,但这无需明确指出。


限制诊断范围


对于每个测试,您可以指定@scope <name>


  • @scope all默认值,验证无处不在;
  • @scope root仅在顶层启动;
  • @scope local仅在函数和方法内部运行。

假设我们要在函数主体外部报告return值。 在PHP中,这有时是有道理的-例如,当从函数连接文件时...但是在本文中,我们对此予以谴责。


 /** * @warning don't use return outside of functions * @scope root */ return $_; 

让我们看看该规则的行为:


 <?php function f() { return "OK"; } return "NOT OK"; // Gives a warning class C { public function m() { return "ALSO OK"; } } 

同样,您可以请求使用*_once而不是requireinclude


 /** * @maybe prefer require_once over require * @scope root */ require $_; /** * @maybe prefer include_once over include * @scope root */ include $_; 

现在,在匹配模式时,并不会一直考虑括号。 模式(($x))不会找到“括号中的所有表达式”,而只是忽略括号的任何表达式。 但是, $x+$y*$z($x+$y)*$z按其应有的方式工作。 此功能来自使用令牌()的困难,但是有可能在下一个发行版中恢复该顺序。

分组模板


当模板上出现重复的docdoc注释时,可以组合模板。


一个简单的例子来演示:


变成了(有分组)
 / ** @也许不使用退出或死亡* /
死($ _);

 / ** @也许不使用退出或死亡* /
退出($ _);
 / ** @也许不使用退出或死亡* /
 {
  死($ _);
  退出($ _);
 }

现在,假设在以下示例中没有此功能的描述规则将是多么不愉快!


 /** * @warning don't compare arrays with numeric types * @type array $x * @type int|float $y * @or * @type int|float $x * @type array $y */ { $x > $y; $x < $y; $x >= $y; $x <= $y; $x == $y; } 

本文中指定的记录格式只是建议的选项之一。 如果您想参与选择,那么您将有这样的机会:您需要为那些比其他人更喜欢的报价提供+1。 有关更多详细信息, 请单击此处


动态规则如何整合



在启动时,NoVerify尝试查找rules参数中指定的规则文件。


接下来,将此文件解析为常规的PHP脚本,并从生成的AST中收集一组绑定了phpgrep模板的规则对象。


然后,分析器根据通常的方案开始工作-唯一的区别是,对于某些已检查的代码段,它启动了一组绑定规则。 如果触发了规则,则会显示警告。


成功被认为是phpgrep模板与至少一个过滤器集(它们用@或分隔)通过的@or


在这个阶段,即使有很多动态规则,规则机制也不会显着降低Linter的运行速度。


匹配算法


使用天真的方法,对于每个AST节点,我们需要应用所有动态规则。 这是一个非常低效的实现,因为大部分工作都是徒劳的:许多模板都有一个特定的前缀,我们可以通过这些前缀对规则进行聚类。


这类似于并行匹配的想法,但是我们不会诚实地构建NFA,而只是“并行化”计算的第一步。


考虑具有三个规则的示例:


 /** @warning duplicated then/else parts of ternary */ $_ ? $x : $x; /** @warning don't call explode with delim="" */ explode("", ${"*"}); /** @maybe suspicious empty body of the if statement */ if ($_); 

如果我们有N个元素和M条规则,那么采用幼稚的方法,我们将要执行N * M个操作。 从理论上讲,可以将这种复杂度降低为线性并得到O(N) -如果将所有模式组合为一个并按其执行匹配,例如Go的regexp包。


但是,实际上,到目前为止,我一直专注于这种方法的部分实现。 它将允许将上面文件中的规则分为三类,并为没有规则对应的那些AST元素分配第四个空类别。 因此,每个元素最多执行一条规则。


如果我们有成千上万的规则,并且会感觉到速度明显下降,则算法将最终确定。 同时,解决方案的简单性和所带来的加速适合我。


选择的折磨,或@type形式的一点点


任务:为phpdoc注释中的过滤器选择良好的语法。

当前语法复制@var@var ,但是我们可能需要新的运算符,例如,“ type is not equal”。 想象一下它的外观。


我们至少有两个重要优先事项:


  1. 注释的可读性和简洁性。
  2. IDE可以提供最大的支持,而无需额外的努力。

对于PhpStorm,有一个php-annotations插件,可添加自动完成功能,过渡到注释类以及使用phpdoc注释的其他实用性。


在实践中,优先级(2)意味着您所做的决策与IDE和插件的期望没有矛盾。 例如,您可以使用php-annotations插件可以识别的格式进行注释:


 /** * Type is a filter that checks that $value * satisfies the given type constraints. * * @Annotation */ class Filter { /** Variable name that is being filtered */ public $value; /** Check that value type is equal to $type */ public $type; /** Check that value text is equal to $text */ 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 /** @warning define with 3 arguments */ define($_, $_, $_); 

我们希望获得大致相同的结果-以便尽可能简单地完成琐碎的事情。


结论



链接,有用的材料


这里收集了重要的链接,本文中可能已经提到了一些重要的链接,但是为了清楚和方便起见,我将它们收集在一个地方。



如果您需要更多可以实施的规则示例,则可以浏览NoVerify测试

Source: https://habr.com/ru/post/zh-CN473718/


All Articles