有很好的PHP静态分析实用程序:PHPStan,Psalm,Phan,Exakat。Linters可以很好地完成工作,但速度却很慢,因为几乎所有的东西都是用PHP(或Java)编写的。对于个人使用或小型项目,这是正常的,但是对于拥有数百万用户的网站,这是一个关键因素。速度慢的linter会减慢CI管道的速度,使其无法用作可集成到文本编辑器或IDE中的解决方案。
一个拥有数百万用户的网站是VKontakte。开发和添加新功能,测试和修复错误,复审-在艰难的期限内,所有这些工作都应该迅速进行。因此,可以在5到10秒钟内检查500万行代码库的良好快速的Linter是不可替代的。 市场上没有合适的棉短绒,因此Yuri Nasretdinov(你摇滚)从VKontakte向开发团队-NoVerify写了他的帮助。这是用Go编写的PHP的linter。它的工作速度是同类产品的10到30倍,可以找到PhpStorm不会警告的内容,可以轻松扩展并很好地集成到以前从未听说过静态分析的项目中。 伊斯坎德•莎里波夫(Iskander Sharipov)会讲讲这羽短绒棉衣。切入点:他们如何选择短绒棉衫并更喜欢编写自己的短绒棉衫,为什么NoVerify这么快,它是如何从内部排列的,为什么要用Go编写,它可以找到什么以及如何扩展,必须为此做出什么妥协以及可以基于它来构建。伊斯坎德尔·沙里波夫(准电解质)在VKontakte的后端基础架构中工作,并且对NoVerify十分熟悉。过去,他曾在英特尔团队中从事过Go编译器工作。他不是用PHP编写的,但这是他最喜欢的静态分析语言-其中有很多地方可能出错。注意。要了解背景知识,请阅读NoVerify onHabré的作者Yuri Nasretdinov的文章,并提供背景知识并与通常使用PHP编写的一些短绒棉进行比较。所有关于PHP的声明(在Yuri和此处的文章中)都是一个笑话。伊斯坎德尔(Iskander)喜欢PHP,每个人都喜欢PHP。产品开发
在VKontakte中,这是KPHP上的网站开发。速度对于VKontakte很重要:修复错误,从第一阶段到最后阶段添加和开发新功能。但是,速度伴随着错误,尤其是在期限紧迫的情况下-我们急于忙碌,紧张,犯下的错误要比平静的情况多。错误会影响用户。我们不希望他们受苦,因此我们控制质量。但是质量控制减慢了发展速度。我们也不希望这样做,因此必须使效果最小化。为此,我们可以进行更多的代码复查,并雇用更多的测试人员并编写更多测试。但是,所有这些操作的自动化程度都很差:必须进行审查,并且必须编写测试。我团队的主要任务是不同的。收集指标,分析并快速修复。如果出现问题,我们希望快速回滚,了解问题所在,进行修复,然后将有效代码重新添加到生产环境中。监视管道的严格性,以使完全无法运行的代码完全不投入生产-您无需回滚它。在这里,lint可以提供帮助-静态代码分析器。我们将讨论这个。选择一个短绒
让我们选择一个添加到管道中的短绒。我们采用一种简单的方法-制定需求。短绒棉应该工作很快。我们的流程有几个步骤:在等待反馈的过程中,lint的操作不应花费太多时间和时间。支持“他们的”检查。短绒很可能没有我们需要的所有东西,我们将不得不添加我们自己的支票。他们应该找到我们代码库中的典型问题,从我们的项目角度检查代码。测试并不能(或方便地)涵盖所有这些内容。支持“自有”支票。我们可以编写许多测试,但是它们会得到很好的支持吗?例如,如果我们编写正则表达式,则当您需要考虑语言的上下文,语义和语法时,它们将变得更加复杂。因此,测试不是一种选择。我们审查的大多数短毛猫都是用PHP编写的。但是它们并不能按需传递。PHP中的Linters(尚无AOT编译)的工作速度比其他语言慢10到20倍-我们最大的文件可以分析数十秒。这会大大减慢工作流程的速度,这是一个致命的缺陷。在这种情况下,开发人员会做什么?他们写自己的。因此,我们在Go上编写了NoVerify PHP linter。为什么呢?剧透:不仅因为汝拉决定。规范化
Go是开发速度和生产率之间的良好折衷。
图片中带有“信息图表”的第一个“证明”:良好的执行速度,易于支持。我们的发展速度有所下降,但前两点对我们而言更为重要。这些数字是取自头顶的,没有任何后盾。对于第二个“证据”,他的论点更为简单。PHP较慢,Go较快,依此类推。 我们选择Go的原因有三个。从根本上来说,将Go语言用作实用程序的语言很容易学习。当然,在PHP开发团队中,有人听说过Go,看着Docker,知道它是用Go编写的,甚至看到了源代码。基本了解之后,经过一两个星期的强化Go学习,他们将能够在上面编写代码。围棋非常有效。即使是初学者也不会犯很多错误,因为Go具有良好的调优能力和许多轻巧的特性。在Go中,平均代码比其他语言要好一些,因为可以用更少的方法来射击自己的腿。Go应用易于维护。Go是一种相当成熟的编程语言,几乎所有您想要的开发人员工具都可以使用。我们将根据需求验证NoVerify。- 为此,您可以编写扩展,包括开源和您自己的。我们必须分开这些检查,并且您可以编写自己的检查,这一点很重要。
- 易于测试和开发。部分是因为标准的Go发行版具有带有概要分析和测试的标准框架。它主要用于单元测试。像我们一样,可以使用特别灵巧的集成-我们通过Go测试编写了集成测试。
整合的妥协
让我们从问题开始。当您首次在未使用任何分析的旧项目上启动任何linter 时,您很可能会看到此情况。
哦,我的代码!没有人会纠正这么多错误。我要关闭该项目,删除短绒,并且不再运行它。如何避免这种情况?整合
在差异模式下运行。我们不希望在整个配置项中每个CI步骤都运行一百万个错误的所有检查。也许您知道基线:在NoVerify中,这是开箱即用的,您不需要嵌入单独的实用程序。我们立即认为,需要这样一种制度。将旧的(供应商)添加到例外中。不碰某些东西,即使有缺陷也可以搁置一旁,以免自己修改,不留下历史痕迹。我们从检查的一部分开始。您无法连接包括样式在内的所有内容。首先,它会发现真正的错误:我们将找到,修复并切换到新的错误。我们收集同事的反馈。如何理解何时该开启其他功能?问同事。一旦他们高兴地发现错误已消失并且几乎没有发现任何错误,就打开其他东西-是时候开始工作了。Git设置
差异模式意味着您拥有版本控制系统-Git。如果您有SVN,那么说明将无济于事,请转至Git。我们使用短绒棉纸安装预推钩,并在开始代码之前进行检查。我们在本地计算机上检查是否有--no-verify
绕过linter的选项。使用预接收钩子并禁用服务器端linter可能会更方便,但是由于历史原因,VK中的预推钩子中发生了很多事情,因此NoVerify也内置在其中。推送后,将启动CI检查。 NoVerify有两种操作模式:全面分析和不全面分析。在CI上,您最有可能想要(并且可以)运行--git-full-diff
-CI中的计算机可以更重地加载,甚至可以检查那些未更改的文件。在本地计算机上,我们可以只对已更改的文件运行不太严格但更快的分析(快5-15秒)。 误报
请看下面的示例:在此函数中,接受包含字段的内容,但未对类型进行任何描述。当从不同的上下文中调用函数时,根本就没有字段。在严格的版本中,lint可能会抱怨:“尚不清楚它是什么类型,如何在没有检查的情况下返回字段?” 但这不一定是错误的。function get_foo($obj) {
return $obj->foo;
^^^
}
Warning:
Property "foo" does not exist
误报会干扰。
这是放弃短绒的主要原因。人们选择其他选项可以发现更少的错误,但产生更少的误报。他们经常在某些事情起作用时四处走动,但这并不是一个错误。许多人都有绕过linter的机制-与标志一起运行而不检查linter。在我们国家,这个标志被称为no-verify
预推钩。我们经常使用它,而这个名字就以棉短绒的名字永生。挑剔
另一个短绒物产。例如,许多人不理解别名。在PHP中,sizeof
这是一个类似物count
:它不计算大小,但返回元素数。C开发人员的思维模式具有sizeof
不同的含义。如果在代码库中sizeof
最有可能是Mean count
。但这是挑剔的。$len = sizeof($x);
^^^^^^
Warning:
use "count" instead of "sizeof"
怎么办呢?
要严格并强制统治一切。施加规则,要求,遵守并不允许其规避-永远行不通。为了使工作如此僵化,团队必须由同一个人组成:性格,文化水平,学究和对代码质量的认识。如果不是这样,将会发生骚乱。从您的克隆中组建一个团队比强迫遵守所有规则要容易。 请勿阻止对评论的推送/提交,例如固定sizeof
为count
。这很可能不是错误,而是挑剔的行为,不会影响代码。但是,然后(团队)将忽略99%的响应,并且代码中总会有多余的响应sizeof
。允许不同团队和开发人员进行某种程度的配置。您可以为每个命令配置配置,以使那些不想更改sizeof
为的人count
无法执行此操作。让其他人遵守规则。一个不错的选择,但一致性会下降,并且在某些目录中,代码会稍差一些。每月在subbotniks上运行一次此类检查。不能每次都在CI或预推钩子上运行检查,而是每月在例行Cron中运行一次。积极开发后,运行并编辑找到的所有内容。但是这项工作需要用于自动化和验证的资源。没做什么。也可以关闭样式检查。妥协
一个快乐的开发人员和一个快乐的linter总是会妥协。让短毛猫高兴很容易:最严格的模式和缺乏解决方法。也许在那之后没有人会留在团队中,所以如果短毛猫干扰了工作,那就是一个问题。首先是有用的动作。
否验证技术细节
私人检查VKontakte。 Noverify是这样写的。在GitHub中,NoVerify存储库分为两个部分:用于实现linter的框架和单独的检查vklints。这样做是为了使linter加载第三方检查:您可以在Go上编写一个单独的模块,然后它们在框架中进行注册。从NoVerify二进制文件开始后,框架将加载所有已注册的检查集,它们将作为一个整体工作。
NoVerify既是库又是二进制文件(lint)。我们的支票称为vklints。他们发现他们看不到PhpStorm和Open Source NoVerify-不适合一般使用的重要错误。什么是vklints?检查使用某些函数,类,甚至不遵循我们约定的全局变量的细节。由于样式指南中所述的各种原因,这是不能在特殊地方使用的东西。其他样式检查。它们与PHP社区中接受的内容不符,没有在PHP标准建议书中描述甚至与之矛盾,但是对我们而言,它是标准。将它们添加到“开源”没有意义,因为您不想关注它们。某些类型的比较要求严格。例如,我们有一项检查,要求使用比较运算符比较字符串===
。特别是,它需要传递一个标志以严格比较功能,以便比较字符串。可疑阵列键。另一个有趣的错误:有时,开发人员在提交时可以在保存文件之前按组合键。这些字符有时保留在字符串或一段代码中。一次,在数组的键中是俄语字母“ Y”。开发人员最有可能在俄语布局中按CTRL-S,保存文件并提交。有时我们会在数组中找到这样的键,但是新的错误将不再存在。动态规则是PHP中描述的一种更简单的NoVerify扩展机制。关于此的另一篇文章已经写到:如何在不编写任何Go代码的情况下向NoVerify添加检查。NoVerify如何运作
要解析PHP,您需要一个解析器。我们不能在PHP中使用PHP解析器:它很慢,从Go开始只能通过C中的包装器使用。因此,我们在Go中使用了解析器。该解析器有几个问题。不幸的是,他只能使用UTF-8,我们需要区分UTF-8而不是UTF-8。除了UTF-8,在俄语PHP项目中经常可以找到Windows-1251。我们也有这样的文件。我们如何识别它们? 该文件encodings.xml
列出了使用UTF-8的文件所在的所有路径。如果我们在这些路径之外遇到文件,那么我们将通过流传输将流传输到UTF-8(无需预先转换)。解析与分析
分几步完成。首先,我们从phpstorm-stubs加载元数据。这是看起来像PHP代码的数据,但是从未执行过,并且描述了标准函数的输入/输出类型。phpStorm元数据具有一个有用的linter 覆盖指令。例如,它允许我们描述我们接受一个类型的数组T[]
并返回一个类型
(对函数有用array_pop
)。phpstorm-stubs首先被加载。我们使用元数据作为初始类型信息-基础。棉绒吸收了这一基础,我们开始分析其来源。我们在吸收之前加载当前的主控。我们以两种方式检查代码:- 本地更改:关于基准,我们在代码中发现了新的错误;
- 我们指出了修订的范围:第一个和最后一个修订,并且它们之间的所有内容都包含在内-这是新代码,“之前”的所有内容都是旧的。
接下来是分析阶段。AST分析。现在,我们有了元数据,类型信息。我们直接在AST之上获取所有PHP源代码,parsim和分析-我们目前没有中间表示。分析原始AST并不是很方便,特别是如果您依赖于它所代表的库和数据类型。 分析结果存储在缓存中。它用于重新分析,速度更快。报告和过滤。然后,我们两次生成报告或警告:首先,我们发现代码的旧版本(基线之前)的警告,然后是新版本的警告。报告通过比较(diff)进行过滤-我们寻找新版本代码中出现的警告,并将其传递给用户。在某些静态分析仪中,这称为“基线模式”。双重代码分析(在差异模式下)非常慢。但是我们负担得起-NoVerify仍然比其他PHP链接器快几十倍。同时,它还有至少30%的额外加速储备。我们如何分析文件?在PHP中,您可以在定义函数之前先调用它-在分析函数之前,您需要了解有关该函数的信息。因此,首先我们遍历AST中的整个文件,建立索引,确定所有函数的类型,注册类,然后再对其进行分析。 分析是文件的第二次传递。大多数解释器和编译器还可以使用两遍或更多遍。为了避免第二次“扫描”文件,您必须在使用前声明,例如在C中。类型推断
最有趣的部分是,这里最常遇到错误。它仍然不符合PHP类型系统的正确性,这很难正式定义。该模型是什么样的。语义模型(演示)。类型类型:- 期望是我们在注释中描述的内容,我们期望程序中有某些类型,但这并不意味着它们确实在程序中使用过。
- 实际 -程序中的实际值。例如,如果我们为某物分配一个数字,则很明显,
int
或float
(如果它是浮点数)将是实际类型。
实际类型似乎“更强”-它们是真实的,真实的。但是有时我们只能通过注释获得类型。注释(预期类型)可以分为两类:信任和不信任。例如,phpstorm-stubs属于第一类。在使用它们之前,它们被视为已审核(无错误)。不可靠的是其他开发人员编写的,因为它们可能有错误。实际类型也可以分为几个部分:值,断言,谓词和类型提示,它们扩展了PHP 7的功能。但是存在类型提示无法解决的问题。预期与实际
假设一个类Foo
有一个继承者。在子孙类中,我们可以调用不在Foo中的方法,因为子孙扩展了父类。但是,如果我们使用返回类型的注释(from )来获得继承人Foo
,则会出现问题。我们可以调用此方法,但是IDE不会提示您-您需要指定。这是PHP中的后期静态绑定,当类继承者无法返回时,则为静态绑定。 new static()
self
static()
Foo
class Foo {
public function newStatic() : self {
return new static();
}
}
当我们写的时候new static()
,不仅班级可以返回new Foo
。例如,如果一个Foo
类是从继承的bar
,则可能存在new bar
。因此,我们至少需要两种类型的信息。它们都不是多余的-都需要。因此,此处的实际类型将是self
-对于PHP解释器。但是,为了使IDE和短毛绒工作,我们需要static
。如果从继承人类的上下文中调用此代码,则需要知道以下信息:它不是同一个基类,并且具有更多方法。class Foo {
public function newStatic() : self {
return new static();
}
}
类型提示
静态类型输入和类型提示不是一回事。
您可能听说过,您只能检查功能的边界。在边界处,我们检查输入和输出,其中输入是函数参数。在函数内部,您可以做任何废话:尽管已描述了foo
值int
,但可以指定一个值T
。您可能会抱怨自己违反了声明的类型,但是对于PHP来说没有error。 declare(strict_types=1);
function f(T $foo) {
$foo = 10;
return $foo;
}
一个例子比较困难-我们回来了foo
吗?在函数开始时,我们确定foo
它是T
,并且没有关于返回值的信息。 declare(strict_types=1);
function f(T $foo) {
$foo = 10;
return $foo;
}
哪种类型正确?前两个,我们将分析它们之间的区别。PhpStorm和linter输出第二个选项。尽管事实总是会返回int
,但T|int
还是会推导出类型- 类型的“联合”。这是一个可以为这两个值分配的类型:首先,我们获得了有关类型T的信息,然后我们为其分配了10
,因此变量的类型foo
必须与这两种类型兼容。注解
可能有评论和注释。
在下面的示例中,我们写道,我们返回一个数字,但返回一个字符串。如果linter仅在注释和类型提示级别起作用,那么我们将认为它总是会返回int
。但是,实际类型只是有助于摆脱这种情况:在这里,期望类型是this int
,而实际类型是字符串。Linter知道该字符串已返回,并可能警告您已承诺返回int
。这种分离对我们很重要。
function f() { return "I lied!"; }
注释的继承。在这里,我的意思是,实现某种接口的类具有一个方法。该方法具有良好的注释,文档,类型-实现接口是必需的。但是在实现过程中没有任何评论:只有@inheritdoc
一无所有。interface IFoo {
public function foo();
}
class Fooer implements IFoo {
public function foo() { return "10"; }
}
什么返回此方法?似乎返回了-接口中描述的内容int
,但实际上是一个字符串。这不好:PHP都是一样的,但是融合对我们很重要。有两个选项可修复此代码。显而易见的是要返回int
。但也许您需要返回其他类型。该怎么办?写下我们返回字符串。在这种情况下,IDE和linter都需要显式类型信息,以便正确分析代码。interface IFoo {
public function foo();
}
class Fooer implements IFoo {
public function foo() { return "10"; }
}
如果人们发表评论,则根本不需要此信息@inheritdoc
。PhpStorm不必了解您拥有的类型。但是,如果类型描述不正确,则会出现问题。当我们对元数据(类型)使用相同的文件时,PhpStorm和linter会有许多不相交的错误。如果我们修复了JetBrains信息库中phpstorm-stub中所需的所有内容,那么IDE很可能会崩溃。如果您默认保留所有内容,那么并不是所有内容都无法正常工作,因此,我们有一个小分支 -VKCOM / phpstorm-stubs。添加了一些补丁程序来修复不适合的问题。对于PhpStorm,我不推荐使用它,但对于短绒棉来说,这是必需的。开源的
Noverify是一个开源项目。它发布在GitHub上。简要说明“如果出了问题”。如果发生故障或无法启动。错误的反应是重新发送并删除NoVerify。正确的反应:在GitHub上发布票证并讨论您的问题。最有可能在1-2天内解决。您缺少某些功能。错误的反应:删除NoVerify并编写自己的Linter(尽管编写自己的Linter总是很酷)。正确的反应:在GitHub上发布票证,也许我们将添加一个新功能。功能要比错误要复杂得多-引起了讨论,每个人对团队中的实现都有不同的见解。但最终,它们仍在实施中。如果您对项目的开发感兴趣,或者只想谈论静态分析,请访问我们的聊天室-noverify_linter。PHP-, , , , PHP Russia.
, . , , . telegram- @PHPRussiaConfChannel. , .