静态代码分析

静态代码分析意味着唯一的信息来源就是代码本身。只需扫描源代码,这些工具就能发现即使是团队中最资深的开发人员在代码审查时也会忽略的问题。

在接下来的章节中,我们将向你介绍这些工具:

  • phpcpd

  • PHPMD

  • PHPStan

  • Psalm

phpcpd – 复制粘贴检测器

复制和粘贴编程可以是任何事情,从简单的烦人到对项目的真正威胁。错误、安全问题和不良做法将被复制,从而变得更难以修复。把它想象成一场在你的代码中传播的瘟疫。

这种形式的编程非常常见,尤其是在经验不足的开发人员中,或者在期限非常紧迫的项目中。幸运的是,我们的干净代码工具包提供了一种补救措施 - PHP 复制粘贴检测器 (phpcpd)

安装和使用

该工具只能以自带 PHP 压缩包 (phar) 的形式下载,因此我们这次不会使用 Composer 来安装它:

$ wget https://phar.phpunit.de/phpcpd.phar
处理 Phar 文件

在第 9 章 “组织 PHP 质量工具” 中,我们将学习如何组织 Phar 文件。目前,只需下载就足够了。

下载后,phpcpd 无需进一步配置即可立即使用。它只需要将目标目录的路径作为参数。下面的示例展示了如何扫描 src 目录以查找所谓的 "克隆"(即被多次复制的代码)。先按默认设置执行:

$ php phpcpd.phar src
phpcpd 6.0.3 by Sebastian Bergmann.

No clones found.

Time: 00:00, Memory: 2.00 MB

如果 phpcpd 没有检测到任何克隆,则值得检查控制其 "挑剔程度" 的两个选项:min-linesmin-tokens

$ php phpcpd.phar --min-lines 4 --min-tokens 20 src
phpcpd 6.0.3 by Sebastian Bergmann.

Found 1 clones with 22 duplicated lines in 2 files:
- /src/example.php:12-23 (11 lines)
/src/example.php:28-39
/src/example3.php:7-18

32.35% duplicated lines out of 68 total lines of code.

Average size of duplication is 22 lines, largest clone has
11 of lines

Time: 00:00.001, Memory: 2.00 MB

最小行数(min-lines)选项允许我们设置一段代码在被认为是克隆代码之前所需的最小行数。

要理解 min-tokens 的用法,我们必须先弄清楚标记的含义:在执行脚本时,PHP 会在内部使用所谓的 "标记化器"(tokenizer)将源代码分割成单个标记。标记是 PHP 程序中的一个独立组件,例如一个关键字、一个运算符、一个常量或一个字符串。把它们想象成人类语言中的单词。因此,min-tokens 选项可以控制一段代码在被视为克隆之前所包含的指令数量。

你可能需要同时使用这两个参数,为你的代码库找到 "挑剔" 的平衡点。代码中存在一定数量的冗余并不会自动成为问题,而且你也不想过多地打扰其它开发人员。因此,使用默认值是一个不错的选择。

更多选择

你还应该了解另外两种选择:

  • --exclude <path>:将路径排除在分析之外。例如,单元测试通常包含大量复制粘贴代码,因此需要排除测试文件夹。如果需要排除多个路径,可以多次给出选项。

  • --fuzzy:有了这个特别有用的选项,phpcpd 就会在检查时混淆变量名。这样,即使变量名被聪明但懒惰的同事更改,克隆也会被检测到。

phpcpd 回顾

虽然 phpcpd 很容易使用,但它对防止项目中复制和粘贴代码的缓慢传播有很大帮助。因此,我们建议你将其添加到你的简洁编码工具包中。

PHPMD:PHP 混乱检测器

PHP 混乱探测器会扫描代码,找出潜在的问题,也就是所谓的 "代码气味"--代码中可能引入错误、意外行为或一般来说较难维护的部分。与代码风格一样,要避免出现问题,也需要遵循一定的规则。混乱探测器就是将这些规则应用到我们的代码中。PHP 生态系统中的标准工具是 PHPMD,我们将在本节中向你展示。

安装和使用

在详细了解这款工具的功能之前,让我们先使用 Composer 安装它:

$ composer require phpmd/phpmd --dev

安装完成后,我们就可以在命令行上运行 PHPMD 了。它需要三个参数:

  • 要扫描的文件名或路径(如 src)。多个位置可以逗号分隔。

  • 应生成报告的以下格式之一:html、json、text 或 xml。

  • 一个或多个内置规则集或规则集 XML 文件(以逗号分隔)。

为了快速开始,让我们扫描 src 文件夹,将输出格式为 text,并使用内置的 cleancodecodesize 规则集。我们可以通过运行以下命令来实现:

$ vendor/bin/phpmd src text cleancode,codesize

PHPMD 会将所有输出写入命令行上的标准输出 (stdout)。不过,除文本外,所有输出格式都不能在这里读取。如果想获得初步概览,不妨使用 html 输出,因为它会生成格式精美的交互式报告。为了将输出存储到文件中,我们将使用 > 操作符将其重定向到文件,如下所示:

$ vendor/bin/phpmd src html cleancode,codesize > phpmd_report.html

只需在浏览器上打开 HTML 文件,就会看到与图 7.1 类似的报告:

image 2023 11 11 17 23 22 859
Figure 1. Figure 7.1: A PHPMD HTML report in a browser

报告是交互式的,因此请务必点击 "显示详细信息" 或 "显示代码" 等按钮,以显示所有信息。

规则和规则集

在前面的示例中,我们使用了内置的 cleancodecodesize 规则集。首先,规则集是根据规则检查的问题域命名的,例如,对于 cleancode 规则,你只能找到有助于保持代码库清洁的规则。不过,你最终还是会发现有很多复杂功能的庞大类。为了避免这种情况,有必要添加 codesize 规则集。

下表列出了可用的规则集及其用法:

Table 1. Table 7.1: PHPMD rulesets
Ruleset Short name Description

Clean code rules

cleancode

总体上强制执行干净的代码

Code size rules

codesize

检查长或复杂的代码块

Controversial rules

controversial

检查存在争议的最佳实践和不良实践

Design rules

design

帮助查找与软件设计相关的问题

Naming rules

naming

避免名称太长或太短

Unused code rules

unused

检测可以删除的未使用代码

使用这些内置规则时,只需将上述简称作为函数调用的参数即可,如上例所示。

如果你有幸在绿色环境中开始一个项目(即从零开始),你可以也应该从一开始就执行尽可能多的规则。这样可以从一开始就保持代码库的整洁。对于现有项目,则需要付出更大的努力,我们将在下一节中看到这一点。

在遗留项目中使用 PHPMD

不过,你往往希望在现有项目中使用 PHPMD。在这种情况下,你很可能会被它在首次运行时抛出的无数警告所淹没。不要放弃,还有一些方法可以帮助你!

调整规则集

如果你计划将 PHPMD 添加到现有项目中,那么全面使用规则集肯定会因为报告的问题太多而导致挫败感。你可能希望一次只专注于一两个规则集。

此外,你也很可能会遇到一些一开始就觉得烦人或适得其反的规则,例如 ElseExpression 规则,它禁止在 if 表达式中使用 else。暂且不讨论这条规则是否有用,重写无数条运行正常的语句是不值得的。因此,如果不想在项目中使用该规则,就需要创建自己的规则集。

规则集是通过 XML 文件配置的,其中指定了属于规则集的规则。每个规则基本上都是一个包含规则逻辑的 PHP 类。下面的 XML 文件定义了一个自定义规则集,其中只包括 cleancodecodesize 规则集:

<?xml version="1.0"?>
<ruleset name="Custom PHPMD rule set"
    xmlns=http://pmd.sf.net/ruleset/1.0.0
    xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
xsi:schemaLocation=http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd
xsi:noNamespaceSchemaLocation=
"http://pmd.sf.net/ruleset_xml_schema.xsd">
    <description>
        Rule set which contains all codesize and cleancode rules
    </description>
    <rule ref="rulesets/codesize.xml" />
    <rule ref="rulesets/cleancode.xml" />
</ruleset>

XML 如今似乎有点过时了,但它仍能很好地发挥作用。通常情况下,你不需要关心 <ruleset> 标签的所有属性,只要确保它们存在即可。<description> 标签可以包含任何你认为能很好描述规则集的文本。

<rule> 标签对我们很重要。在前面的示例中,我们同时引用了 codesizecleancode 规则。

在这一点上,你最好去 GitHub 仓库 https://github.com/phpmd/phpmd/tree/master/src/main/resources/rulesets 中查找内置规则集。由于 XML 是一种相当冗长的文件格式,你会很快熟悉它。

假设我们想从检查中移除上述 ElseExpression 规则。为此,你只需在相应的 <rule> 标记中添加 <exclude> 标记,如下所示:

<rule ref="rulesets/cleancode.xml">
    <exclude name="ElseExpression" />
</rule>

这样,你就可以根据需要从一个规则集中排除尽可能多的规则。如果只想从不同的规则集中挑选某些规则,也可以反其道而行之,直接引用所需的规则。如果想让自定义规则集只包含 StaticAccess 和 UndefinedVariable 规则,XML 文件应包含以下两个标记:

<rule ref="rulesets/cleancode.xml/StaticAccess" />
<rule ref="rulesets/cleancode.xml/UndefinedVariable" />

关于 XML 配置文件,最后一件要了解的重要事情是如何更改规则的各个属性。同样,了解所有属性的好方法是查看实际的规则集文件。另外,你也可以在 https://github.com/phpmd/phpmd/tree/master/src/main/php/PHPMD/Rule 查看每个规则的实际 PHP 类。

一个典型的例子是为 StaticAccess 规则定义例外。避免静态访问通常是一种好的做法,但往往无法避免。假设你的团队同意允许对 DateTimeDateTimezone 对象进行静态访问,你只需按如下方式进行配置即可:

<rule ref="rulesets/cleancode.xml/StaticAccess">
    <properties>
        <property name="exceptions">
            <value>
                \DateTime,
                \DateTimezone
            </value>
        </property>
    </properties>
</rule>

今后要使用自定义规则集,只需将前面的 XML 保存在一个文件(通常称为 phpmd.xml)中,并在下次运行时将其传递给 PHPMD

$ vendor/bin/phpmd src text phpmd.xml
配置文件的位置

通常的做法是将包含要使用的规则集的 phpmd.xml 文件放在项目的根文件夹中,并将其作为配置的唯一来源。如果将来有任何修改,只需调整一个中心文件即可。

抑制警告

处理遗留代码的另一个有用工具是 @SuppressWarnings DocBlock 注解。假设项目中有一个类使用了静态方法调用,而且现在无法更改。默认情况下,任何静态访问都会引发警告。由于你不想在代码中的其它地方使用静态访问,而只想在这个类中使用静态访问,因此删除 StaticAccess 规则会适得其反。

在这种情况下,你可以使用 @SuppressWarnings 注解:

/**
* @SuppressWarnings(PHPMD.StaticAccess)
*/
class ExampleClass {
    public function getUser(int $id): User {
        return User::find($id);
    }
}

如有需要,你可以在一个 DocBlock 中使用多个注解。最后,如果要抑制类上的任何警告,只需使用 @SuppressWarnings(PHPMD) 注解即可。

请注意,使用 Suppress 注解应是最后的手段。在所有地方都添加它是很有诱惑力的。然而,这样做虽然能使输出保持沉默,但却不能解决问题。

接受违规行为

除了在文件级别抑制警告或从规则集中排除规则外,你还可以决定承认现有的违规行为。例如,当你想在传统项目中使用 PHPMD 时,可以决定暂时忽略代码中已有的所有违规。但是,如果新类引入了新的违规行为,就会报告这些违规行为。

幸运的是,PHPMD 提供了一个所谓的基线文件,通过运行以下程序,PHPMD 会为你自动生成基线文件,从而使这项任务变得相当简单:

$ vendor/bin/phpmd src text phpmd.xml --generate-baseline

在前面的命令中,我们预计项目根文件夹中已经存在 phpmd.xml 文件。使用上述命令,PHPMD 将创建一个名为 phpmd.baseline.xml 的文件。

现在,你可以运行以下命令:

$ vendor/bin/phpmd src text phpmd.xml

下一次,PHPMD 将自动检测之前生成的基线文件,并使用它相应地抑制所有警告。不过,如果在新位置引入了新的违规规则,它仍会被检测到并报告为违规。

需要提醒的是:与 @SuppressWarning 注释一样,基线功能并不是一个只用一次就可以在将来安全忽略的工具。有问题的代码块仍会作为技术债务成为项目的一部分,并带来各种负面影响。因此,如果你决定使用基线功能,就应确保将来不会忘记解决这些隐藏的问题。

我们将在本书后面讨论如何处理这些问题。现在,如何不时更新基线文件才是最重要的。同样,PHPMD 也能轻松完成这项任务。只需运行以下程序即可:

$ vendor/bin/phpmd src text phpmd.xml --update-baseline

所有不再存在于代码中的违规行为都将从基线文件中删除。

PHPMD 回顾

除非你是在绿色环境中启动项目,否则 PHPMD 的配置需要更多时间。特别是如果你是在一个团队中工作,你将花费更多时间来争论哪些规则需要使用,哪些规则不需要使用。不过,一旦完成这些工作,你就拥有了一个强大的工具,可以帮助开发人员编写高质量、可维护的代码。

PHPStan – PHP 静态分析器

你可能已经注意到,我们在上一节中介绍过的 PHPMD 并不是非常专门针对 PHP 的,但一般都会考虑到最佳编码实践。当然,这一点非常重要,但我们现在要使用 PHPStan 来分析我们的代码,同时考虑到 PHP 的不良实践。

与所有静态分析工具一样,PHPStan 只能利用从代码中获取的信息进行分析。因此,它对现代面向对象的代码效果更好。例如,如果代码大量使用了严格类型,分析器就会有更多信息需要处理,从而返回更多结果。但对于较老的项目,它也会提供巨大的帮助,我们将在下一节中看到这一点。

安装和使用

使用 Composer 安装 PHPStan 也只是一句简单的话:

$ composer require phpstan/phpstan --dev

与大多数代码质量工具一样,PHPStan 也可以使用 PHAR 安装。不过,只有在使用 Composer 时才能安装扩展。我们将在本节稍后部分介绍这些扩展。

让我们使用以下简化示例,并将其存储在 src 文件夹中:

<?php

class Vat
{
    private float $vat = 0.19;

    public function getVat(): int
    {
        return $this->vat;
    }
}

class OrderPosition
{
    public function getGrossPrice(float $netPrice): float
    {
        $vatModel = new Vat();
        $vat = $vatModel->getVat();

        return $netPrice * (1 + $vat);
    }
}

$orderPosition = new OrderPosition();
echo $orderPosition->getGrossPrice(100);

要执行扫描,需要指定 analyse 关键字以及要扫描的路径,在我们的例子中就是 src

$ vendor/bin/phpstan analyse src

图 7.2 显示了 PHPStan 的输出结果:

image 2023 11 11 22 42 15 842
Figure 2. Figure 7.2: An example output of PHPStan

当我们执行 PHP 脚本时,它将输出 100。不幸的是,这是不正确的,因为将 19% 的税金加到净价上,返回值应该是 119,而不是 100。因此,一定是某个地方出了问题。让我们看看 PHPStan 如何帮助我们。

规则级别

PHPMD 中,你可以详细配置要应用的规则,而这里则不同,我们将使用不同的报告级别。这些级别由 PHPStan 开发人员定义,从 0 级(仅执行基本检查)到 9 级(对问题非常严格)。为了避免用户一开始就被错误淹没,PHPStan 默认使用级别 0,即只执行很少的检查。

你可以使用级别 (-l|--level) 选项指定级别。让我们试试下一个最高级别:

$ vendor/bin/phpstan analyse --level 1 src

使用级别方法,你可以毫不费力地逐步提高代码质量,下面我们将用一个编造的示例来证明这一点。第 1 级和第 2 级也不会返回任何错误。不过,当我们最终到达第 3 级时,就会发现问题所在:

image 2023 11 11 22 44 50 054
Figure 3. Figure 7.3: PHPStan reports one error with level 3

再次检查我们的代码,我们可以很快发现问题所在:getVat() 方法返回一个浮点数(0.19),但使用 int 返回类型会将其转换为 0。

严格类型

如果我们在示例代码的顶部添加 declare(strict_types=1); 语句来使用严格模式,PHP 就会抛出一个错误,而不是默默地将返回值转换为 int

这充分体现了静态代码分析的魅力和威力:修复这个小错误就能让我们的代码按照预期运行,而且只需要几秒钟就能完成,因为我们还在开发环境中。但是,如果这个错误进入了生产环境,我们就需要花费更长的时间来修复,而且还会留下一些愤怒的客户。

配置

你可以使用配置文件来确保始终检查同一级别和同一文件夹。配置是用 NEON (https://ne-on.org/) 编写的,这是一种与 YAML 非常相似的文件格式;如果你能读写 YAML,就能正常工作。

基本配置只包含要扫描的级别和文件夹:

parameters:
    level: 4
    paths:
        - src

最好将此配置保存在项目根目录下名为 phpstan.neon 的文件中。这是 PHPStan 默认的位置。如果遵循这一惯例,下次运行时只需指定所需的操作即可:

$ vendor/bin/phpstan analyse

如果使用上述示例配置,PHPStan 现在将使用从 0 级到 4 级的所有规则扫描 src 文件夹。

这还不是你可以在此配置的全部内容。在下一节中,我们将了解一些其它参数。

在遗留项目中使用 PHPStan

如果你想在已有一定年限的项目中使用 PHPStan,根据所选级别的不同,很可能会出现数百甚至数千个错误。当然,你也可以决定继续使用较低的级别;但这也意味着分析器会漏掉更多的错误,不仅是现有的错误,还有新的或修改过的代码中的错误。

在理想的情况下,你可以从第 0 级开始,解决所有错误,然后继续第 1 级,解决所有新错误,以此类推。但这需要大量时间,而且如果没有自动测试,最后还需要进行一次完整的手动测试。你可能没有那么多时间,所以让我们看看还有什么其它选择。

有两种方法可以让 PHPStan 忽略错误:一是使用 PHPDocs 注释,二是在配置文件中使用特殊参数。

使用 PHPDocs 注释

要忽略一行代码,只需在受影响的行前或行上添加注释,使用特殊的 @phpstan-ignore-next-line@phpstan-ignore-line PHPDocs 注释即可:

// @phpstan-ignore-next-line
$exampleClass->foo();
$exampleClass->bar(); // @phpstan-ignore-line

这两行代码都不会再被扫描出错。你可以选择自己喜欢的方式。不过,你无法忽略更大的代码块,甚至整个函数或类(除非你想在每一行都添加注释)。

使用ignoreErrors参数

PHPDocs 注释非常适合在少数几个位置进行快速修复,但如果要忽略大量错误,则需要触及许多文件。不过,在配置文件中使用 ignoreErrors 参数并不方便,因为你必须为想要忽略的每个错误编写正则表达式。

下面的示例将解释它是如何工作的。假设我们不断收到如下错误:

Method OrderPosition::getGrossPrice() has no return type specified.

虽然理论上这很容易解决,但为了避免任何副作用,团队决定不添加类型提示。OrderPosition 类写得很糟糕,也没有测试,但仍能按预期运行。既然它很快就会被替换,我们也不想冒险去碰它。

要忽略这个错误,我们需要在 phpstan.neon 配置文件中添加 ignoreErrors 参数:

parameters:
    level: 6
    paths:
      - src
    ignoreErrors:
      - '#^Method OrderPosition\:\:getGrossPrice\(\) has no return type specified\.$#'

我们不需要定义要忽略的规则或规则集,而是需要在这里提供一个正则表达式,与应忽略的错误信息相匹配。

编写正则表达式很有难度。幸运的是,PHPStan 网站提供了一个非常有用的小工具,可以从错误信息中生成必要的 phpstan.neon 部分: https://phpstan.org/user-guide/ignoring-errors#generate-anignoreerrors-entry

下次运行时,无论错误出现在何处,都将不再显示,因为它符合此处的正则表达式。

PHPStan 不会通知你错误已被忽略。请不要忘记在某个时候修复它们!不过,如果你随着时间的推移进一步改进代码,PHPStan 会在不再匹配被设置为忽略的错误时通知你。这时你就可以放心地将它们从列表中删除了。

如果你想完全忽略某些错误,但只是忽略一个或多个文件或路径中的错误,可以使用稍有不同的符号来实现:

ignoreErrors:
  -
    message: '#^MethodOrderPosition\:\:getGrossPrice\(\) has no return type specified\.$#'
    path: src/OrderPosition.php

该路径需要相对于 phpstan.neon 配置文件的位置。给定该路径后,只有在 OrderPosition.php 中出现的错误才会被忽略。

Baseline

正如上一节所述,在配置文件中手动添加希望忽略的错误是一项繁琐的工作。不过还有一种更简单的方法:与 PHPMD 类似,执行以下带有 --generate-baseline 选项的命令,就可以一次性将所有当前错误自动添加到忽略的错误列表中:

$ vendor/bin/phpstan analyse --generate-baseline

新生成的文件 phpstan-baseline.neon 与配置文件位于同一目录下。不过,PHPStan 不会自动使用它。你必须在 phpstan.neon 文件中手动包含该文件,如下所示:

includes:
  - phpstan-baseline.neon

parameters:
  …

下次运行 PHPStan 时,之前报告的错误应该不会再报告了。

在内部,基线文件只是一个自动创建的 ignoreErrors 参数列表。你可以根据需要随意修改。你可以再次使用 --generate-baseline 选项执行 phpstan 来重新生成它。

扩展

PHPStan 的功能可以扩展。活跃的社区已经创建了大量有用的扩展。例如,Symfony、Laminas 或 Laravel 等框架经常使用魔法方法(如 __get()__set() ),而这些方法无法自动分析。这些框架有一些扩展,可以为 PHPStan 提供必要的信息。

虽然我们无法在本书中介绍这些扩展,但我们鼓励你查看扩展库: https://phpstan.org/user-guide/extension-library 。此外,还有针对 PHPUnit、phpspec 和 WordPress 的扩展。

PHPStan 回顾

PHPStan 是一款功能强大的工具。我们无法在短短几页纸上介绍它的所有功能,但我们已经让你对如何开始使用它有了一个很好的概念。熟悉其基本用法后,请访问 https://phpstan.org 了解更多信息!

Psalm:PHP 静态分析 linting 机器

我们要介绍的下一个也是最后一个静态代码分析器是 Psalm。它将检查我们的代码库是否存在所谓的问题,并报告任何违规行为。此外,它还能自动解决其中的一些问题。那么,让我们仔细看看吧。

安装和使用

同样,使用 Composer 安装 Psalm 也只需敲几下键盘:

$ composer require --dev vimeo/psalm

它还可以以 phar 文件的形式提供。

安装后,我们不能直接启动,而是需要先为当前项目设置一个配置文件。我们可以使用舒适的 --init 选项来创建配置文件:

$ vendor/bin/psalm --init

该命令将在当前目录(项目根目录)下写入一个名为 psalm.xml 的配置文件。在创建过程中,Psalm 会检查是否能找到任何 PHP 代码,并决定从哪个错误级别开始。运行 Psalm 不需要其它选项:

$ vendor/bin/psalm

配置

配置文件已在安装过程中创建,例如,可以类似于下面的配置文件:

<?xml version="1.0"?>
<psalm
    errorLevel="7"
    resolveFromConfigFile="true"
    xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
    xmlns=https://getpsalm.org/schema/config
    xsi:schemaLocation=https://getpsalm.org/schema/config/vendor/vimeo/psalm/config.xsd
>
    <projectFiles>
        <directory name="src" />
        <ignoreFiles>
            <directory name="vendor" />
        </ignoreFiles>
    </projectFiles>
</psalm>

让我们来看看 <psalm> 节点的属性。你无需关心模式和命名空间相关信息,只需关心以下两点:

  • errorLevel:级别从 8(基本检查)到 1(非常严格)不等。换句话说,级别越低,应用的规则越多。

  • resolveFromConfigFile(从配置文件解析):设置为 true 时,Psalm 将从配置文件的位置解析所有相对路径(如 srcvendor),因此通常是从项目根目录解析。

Psalm文档

Psalm 还提供许多配置选项,我们无法在本书中一一介绍。我们建议你一如既往地查阅文档 (https://psalm.dev/docs) 以了解有关该工具的更多信息。

<psalm> 节点中,你可以找到更多设置。在上一个示例中,Psalm 被告知只扫描 src 文件夹,而忽略 vendor 文件夹中的所有文件。忽略 vendor 文件夹非常重要,因为我们不想扫描任何第三方代码。

在遗留项目中使用 Psalm

现在我们来看看如何调整 Psalm 以更好地处理现有项目。与之前的工具一样,忽略问题基本上有两种方法:使用配置文件或 docblock 注释。

有三种代码问题级别:infoerrorsuppress。如果发现了小问题,info 只会打印信息,而错误类型的问题则需要你积极处理。抑制类型的问题则完全不会显示。

持续集成

在构建持续集成管道时,infoerror 之间的区别变得更加重要。info 问题会让构建通过,而 error 问题则会破坏构建。我们稍后将仔细研究这一主题。

Docblock 抑制

@psalm-suppress 注解既可以用在函数 docblock 中,也可以用在下一行的注释中。前面示例中的 Vat 类可以如下所示:

class Vat
{
    private float $vat = 0.19;

    /**
     * @psalm-suppress InvalidReturnType
     */
    public function getVat(): int
    {
        /**
         * @psalm-suppress InvalidReturnStatement
         */
        return $this->vat;
    }
}

配置文件抑制

如果要抑制问题,我们需要为它们配置 issueHandler,在这里我们可以手动设置要抑制的类型。这可以通过在配置文件中的 <psalm> 节点内添加 <issueHandler> 节点来实现:

<issueHandlers>
    <InvalidReturnType errorLevel="suppress" />
    <InvalidReturnStatement errorLevel="suppress" />
</issueHandlers>

上述配置将抑制整个项目中的所有 InvalidReturnTypeInvalidReturnStatement 问题。不过,我们还可以做得更具体一些:

<issueHandlers>
    <InvalidReturnType>
        <errorLevel type="suppress">
            <file name="Vat.php" />
        </errorLevel>
    </InvalidReturnType>
    <InvalidReturnStatement>
        <errorLevel type="suppress">
            <dir name="src/Vat" />
        </errorLevel>
    </InvalidReturnStatement>
</issueHandlers>

在文档 ( https://psalm.dev/docs/running_psalm/dealing_with_code_issues/ )中,你可以找到更多抑制问题的方法,例如通过变量名。

Baseline

与我们之前讨论过的静态代码分析器一样,Psalm 也提供了生成基线文件的功能,该文件将包含当前的所有错误,以便在下次运行时忽略它们。请注意,基线功能只适用于错误问题,不适用于信息问题。让我们先创建文件:

$ vendor/bin/psalm --set-baseline=psalm-baseline.xml

Psalm 没有为该文件设置默认名称,因此需要将其作为选项传递给命令:

$ vendor/bin/psalm --use-baseline=psalm-baseline.xml

你还可以将其作为附加属性添加到配置文件中的 <psalm> 节点:

<psalm
    ...
    errorBaseline="./psalm-baseline.xml"
>

最后,你可以更新基线文件,例如,在你对代码进行了一些改进之后:

$ vendor/bin/psalm --update-baseline

自动修复问题

Psalm 不仅能发现问题,还能自动修复许多问题。在这种情况下,它会通知你,你可以使用 --alter 选项:

Psalm can automatically fix 1 issues.
Run Psalm again with
--alter --issues=InvalidReturnType --dry-run
to see what it can fix.

让我们按照 Psalm 的建议执行命令:

$ vendor/bin/psalm --alter --issues=InvalidReturnType --dry-run

--dry-run 选项会告诉 Psalm 只向你显示它要修改的内容,而不应用这些修改。这样,你就可以检查更改是否正确:

image 2023 11 12 09 00 53 422
Figure 4. Fig 7.4: Psalm showing proposed changes

如果删除 --dry-run 选项,更改将被应用。

Psalm 回顾

Psalm 是简洁代码编写者工具包中的标准工具,这是有道理的。它快速、易用、功能强大。此外,它的代码处理功能还能为你节省大量时间。当然,它与 PHPStan 有许多相似之处,但你经常会发现这两个工具在同一代码库中协同工作时不会出现问题。至少,你应该考虑试一试。