bamboohr/guardrail

BambooHR 自家开发的 PHP 静态分析工具

v0.9.8 2022-08-15 16:03 UTC

This package is not auto-updated.

Last update: 2024-09-15 22:37:17 UTC


README

版权所有 (c) 2017-2024 BambooHR

Latest Stable Version Total Downloads Latest Unstable Version License composer.lock

介绍

Guardrail 是一个针对 PHP 8.3 的静态分析引擎。Guardrail 将索引您的代码库,学习每个符号,然后确认系统中的每个文件都使用这些符号的方式是合理的。例如,如果您调用了一个未定义的函数,Guardrail 将会找到它。

Guardrail 并不能证明您的代码是完美的或甚至是语义上有效的。您永远不应该用 Guardrail 作为不编写单元测试的借口。相反,它是一个最终的保护层,以确保可预防的错误、语法错误或拼写错误不会发生。您可以将 Guardrail 想象成高速公路上的护栏,您永远不会想撞到它们,但您很高兴知道它们在那里。

在 BambooHR,我们是持续集成的坚定支持者。我们在开源 CI 工具 Rapid 中使用了 Guardrail。(见 https://github.com/BambooHR/rapid)这是在运行一系列单元和集成测试的基础上完成的,我们还对这些堆栈的所有层都进行了测试。

Guardrail 使用 Nikita Popov 的优秀 PHP 解析库。(见 https://github.com/nikic/PHP-Parser

PHP 中需要像 Guardrail 这样的工具的原因

根据 W3Techs(《https://w3techs.com/technologies/overview/programming_language/all》)的数据,2017 年,PHP 运行在所有能够确定服务器端语言的网站的 82%。其他文档也证实,互联网上大部分动态内容都是由 PHP 提供的。PHP 为 Facebook 和 Wikipedia 等大型网站提供动力。

通常,这些网站从一个小型的本地代码库开始,比如 WordPress 安装,或者是在框架上的一些定制。这些都是很好的选项,能够发挥 PHP 的优势。您可以快速搭建一个网站,在花费大量时间和金钱担心企业级之前,验证业务模式。PHP 语言性能相当好,开发速度快。语言非常宽容,有一个非常成熟的库生态系统,包括 Composer,几个强大的框架,以及广泛的托管可用性。

对于小型网站,PHP 工作得非常好。如果您很幸运,之前的小型网站已经发展壮大,您将开始遇到处理 PHP 中大型代码库的困难。许多这些复杂性是由于 PHP 是一种弱类型语言。语言中缺乏对契约的强制执行使得难以了解任何给定变量的预期内容。在小团队和小型代码库中,这没问题。在大型团队或大型代码库中,这变得难以管理。此外,当您开始使用更严格的 PHP 类型改进时,您会发现错误直到运行时才报告。在发布之前知道应用程序中存在错误会更好。

Guardrail 是一个允许您找到应用程序中一些错误子集的工具。如果您大量使用类型提示,您会发现 Guardrail 使您能够非常严谨。它可以应用于任何 PHP 5 - PHP 7 代码库。

支持的检查

Guardrail按名称分类检查。以下是错误的标准列表。请注意,所有Guardrail错误都以“标准”一词开头。自定义插件应以不同的字符串开头。(理想情况下,为创建插件的机构命名。)

Guardrail支持高级PHP特性,例如特性、接口、匿名函数和类等。

此外,存在一个简单的插件系统,允许您注册节点访问者以启用抽象语法树的附加检查。在BambooHR,我们使用此插件机制来运行一些仅与我们堆栈相关的附加检查。

限制

  • Guardrail假设所有类和函数都在所有位置可用。它不会检查您的自动加载器或require语句以确认您确实在特定上下文中加载了源文件。
  • PHP允许您在另一个函数内部声明一个函数。这个嵌套函数实际上具有全局作用域,但只有在外部函数执行之后才能可见。Guardrail不支持这种用法模式。
  • Guardrail不会条件性地处理函数。如果函数是在顶级定义或在函数内部嵌套定义的,则它将被索引并被认为是全局可用的。
  • Guardrail依赖于反射来确定内部PHP方法和函数的可用性。您应该在代码预期运行的相同环境中运行Guardrail。请注意,PHP命令行安装通常使用与fastcgi/modphp配置不同的配置文件(因此,使用不同的扩展)。如果您正在测试一个网站,请确保您的CLI配置加载与服务器配置相同的扩展。
  • Guardrail能够进行简单的类型推断。如果您的变量肯定只包含一种类型的数据,则将在该变量上强制执行检查。如果变量可能包含多个不同的值,则Guardrail必须假设您正在正确使用该变量。

要求

  • 需要PHP 7.3、Gzip扩展和Composer。
  • 内存越多越好。适度大的代码库可以使用高达500MB。
  • 在PHP 7和8中运行速度更快。

安装

Guardrail作为BambooHR/Guardrail的composer包可用。

它将自动安装到vendor/bin/guardrail.php。

您还可以通过在vendor/bamboohr/guardrail/src/bin中找到的Build.sh运行将Guardrail打包为.phar文件。

用法

Guardrail的执行有两个阶段:索引和分析。

索引

索引阶段只能在单个进程中运行。包括所有供应商库的适度大的代码库可能需要几分钟才能完成索引。

分析

一旦生成索引,就可以运行分析。分析高度依赖于CPU。
它可以在多个进程或甚至多台机器上运行。当在多台机器上运行时,您需要收集所有机器的输出以查看结果。(BambooHR使用Rapid来自动化此过程。)

配置

Guardrail配置由7个部分组成:选项、索引、忽略、测试、测试忽略、输出和插件。

选项部分是“可选的”。目前,它允许您根据DocBlocks启用类型推断。通常,代码库中会有很多实际上引用了不存在或不正确命名空间类型的DocBlocks。默认情况下,DocBlocks将不会用于类型推断。如果您启用DocBlocks,则Guardrail可以在检查方面更加彻底。请参阅下面的选项部分以了解可以定义的选项。

索引部分是要索引的子目录列表。忽略部分是要从索引中忽略的文件路径列表。忽略部分可以使用globbing模式,包括双星号以指示任何数量的目录。

这两个部分共同确定哪些文件将被索引。
在索引目录下列出但未由一个忽略块排除的任何文件都将被索引。尽可能多地索引你的代码库非常重要,否则将无法解决包含问题。

测试部分是运行分析阶段的目录列表。
测试忽略是分析过程中要忽略的文件路径列表。此部分还可以使用通配符模式一次性忽略多个文件。

输出部分用于控制报告哪些错误。大多数代码库在发出所有标准检查之前都不会通过。我们建议一次添加一个检查,并逐步改进你的代码库,直到所有测试通过。如果输出字符串以"."结尾,则匹配模式中".""之前的任何规则都视为匹配并将输出。例如:emit: ["Standard.Security.*"]以输出所有安全警告。

插件部分是分析时可以使用的大量插件。插件允许你通过自己的检查扩展Guardrail。

示例配置文件

{
	"options": {
		"DocBlockReturns" : true,
		"DocBlockParams" :  true,
		"DocBlockInlineVars" : true,
		"DocBlockProperties": true
	},
    "index": [
        "app",
        "vendor",
        "/usr/share/php"
    ],
    "ignore": [
             "**/vendor/**/tests/**/*",
             "**/vendor/**/Tests/**/*"
    ],
    "test": [
         "app/html",
         "app/includes"
     ],
    "test-ignore": [ 
        "**/vendor/**/*" 
    ],
    "emit":
    [
        "Standard.Unknown.Class",
        "Standard.Unknown.Class.Constant",
        "Standard.Unknown.Function",
        "Standard.Unknown.Variable",

        "Standard.Inheritance.Unimplemented",
 
        "Standard.Scope",
        "Standard.Param.Count",
        "Standard.Param.Type",

        "Standard.Switch.Break",
        "Standard.Parse.Error",
        
        {
            "emit": "Standard.Security.Shell",
            "glob": "**/System/**/*",
            "ignore": "**/System/Shell/**/*"
        },
        
        {
            "emit": "Standard.Unknown.Class.Method",
            "when": "new",
            "glob": ["**/app/BambooHR/Events/Routes", "**/app/BambooHR/Silo/DataWarehouse/**/*"],
            "ignore": ["**/test/**/*", "**/app/BambooHR/Silo/Benefits/Shared/Enrollment/**/*"]
        },
        
        "BambooHR.Impossible.Inject"
    ], 
    "plugins": [
        "plugins/guardrail/ImpossibleInjectionCheck.php"
    ]
}

最简单的emit条目是一个简单的字符串,用于标识始终要输出的错误类型。较长形式是嵌套的JSON对象。它可以包含单个glob字符串或glob字符串数组,该文件名必须匹配,可选地,一个忽略字符串或字符串数组以忽略。你可以为每种错误类型定义多个globbing规则。
如果错误通过任何部分,它将被输出。

你还可以通过在你的函数的docblock中添加@guardrail-ignore [type1],[type2]来禁用函数持续时间的错误。(其中[type#]是要禁用的检查的名称。)你禁用的任何检查都不会在该特定函数的分析期间输出。

命令行

注意:命令行使用在v1.0版本中可能会发生重大变化。

Usage: php -d memory_limit=500M vendor/bin/guardrail.php [-a] [-i] [-n #] [-o output_file_name] [-p #/#] config_file

where: -p #/#                               = Define the number of partitions and the current partition.
                                              Use for multiple hosts. Example: -p 1/4

       -n #                                 = number of child process to run.
                                              Use for multiple processes on a single host.  A good rule of thumb is 1 process per CPU core.

       -a                                   = run the "analyze" operation

       -i                                   = run the "index" operation.
                                              Defaults to yes if using in memory index.
                                
       --diff patch_file                    = Allows you to limit results to only those errors occuring on
                                              lines in a particular patch set.  Requires unified diff format taken
                                              from the root directory of the project.  Must set emit { "when": "new" }
                                              for each error that you want to emit in this fashion.                                                                     
                                
       --format format                      = Select choose between "xunit", "text", or "counts"                                 

       -s                                   = prefer sqlite index

       -m                                   = prefer in memory index (only available when -n=1 and -p=1/1)

       -o output_file_name                  = Output results in junit format to the specified filename
       
       --metric-output                      = Output results to the specified filename

       --symbol-table-output                = Output results to the specified filename

       -v                                   = Increase verbosity level.  Can be used once or twice.

       -h  or --help                        = Ignore all other options and show this page.


要索引所有内容,根据config.json文件存储索引到sqlite数据库,请使用以下命令行。

php vendor/bin/guardrail.php -i -s config.json

要运行分析

php vendor/bin/guardrail.php -a -s config.json

如果你想查看索引或分析阶段的进度,请使用-v启用详细输出。

默认情况下,报告以XUnit格式输出到标准输出。如果你希望输出到文件,请使用-o指定输出文件名。

增量扫描

如果你使用Guardrail的--diff patch_file选项,则可以根据补丁集中标识为更改的行过滤你的结果。这对于逐步改进代码库非常有用。例如,

补丁文件必须以Unified diff格式,从你的项目根目录中获取。(即包含你的Guardrail配置文件的目录。)

设置

 
{
	"emit" : "Standard.VariableVariable",
	"when" : "new"
}

以在测试补丁集时仅输出"Standard.VariableVariable"错误。在BambooHR,我们将其连接到我们的RapidCI设置中,以便每个新提交都经过比我们可以在旧代码中实施的更高标准的测试。使用这种方法,你可以随着时间的推移提高代码库的质量。

对象转换

Java或C#之类的语言支持将对象引用从一种类型转换到另一种类型。这允许你将支持多个接口的对象从一种接口转换为另一种接口。对象的本质并没有改变,只是编译器理解它的方式发生了变化。

在PHP中,这种转换是不必要的。如果一个对象有一个具有正确名称的方法,则可以调用它。

对于静态分析的目的,你只应调用接口中记录的方法。如果你正在传递一个实现多个接口的对象,你需要“转换”该对象以访问其中一个接口。
护栏将尊重只包含变量和“instanceof”运算符的简单if()语句的结果。这种改动通常是良性的,因为你永远不希望在没有实现该接口的对象上调用接口方法。

 if($var instanceof Foo) { 
 	$var->fooInterfaceMethod(); // $var assumed to be a "Foo" inside this clause.
 }
 

如果你有一个总是子类型的变量实例,那么你同样可以使用这两种类型转换技术中的任何一种。

 assert($var instanceof Foo); // PHP 7 asserts. 
 

或者

 /** var Foo $var  Typical doc block cast. */
 

链接

插件架构