PHP静态分析工具进阶:配置PHPStan/Psalm的严格模式与自定义规则开发

PHP静态分析工具进阶:配置PHPStan/Psalm的严格模式与自定义规则开发

各位同学,大家好!今天我们来深入探讨PHP静态分析工具PHPStan和Psalm,重点讲解如何配置它们的严格模式以及如何开发自定义规则,以提升代码质量和可维护性。

静态分析的价值

在深入细节之前,先回顾一下静态分析的价值。与动态分析(例如单元测试)不同,静态分析不需要实际运行代码,而是通过分析源代码来发现潜在的错误和代码质量问题。 这意味着可以在开发早期发现问题,避免在运行时出现难以调试的错误。

静态分析可以检测到的常见问题包括:

  • 类型错误: 例如,将字符串传递给需要整数的函数。
  • 未定义的变量: 访问未初始化的变量。
  • 死代码: 永远不会被执行的代码。
  • 潜在的空指针异常: 在可能为null的值上调用方法。
  • 安全漏洞: 例如,SQL注入或跨站脚本攻击 (XSS)。
  • 代码风格问题: 例如,不符合PSR标准的命名或缩进。

PHPStan与Psalm:选择与比较

PHPStan和Psalm是PHP领域最流行的静态分析工具。 它们都提供了强大的类型检查和代码分析功能,但也有一些关键的区别:

特性 PHPStan Psalm
严格程度 可配置,从0(最宽松)到9(最严格) 可配置,通过level和各种配置文件选项
类型推断 较好,能够推断复杂的类型信息 非常优秀,尤其擅长于理解复杂类型和泛型
扩展性 通过扩展可以添加自定义规则 通过插件可以添加自定义规则和类型定义
性能 通常更快,尤其是在大型项目中 在大型项目中可能较慢,但最新的版本做了很多优化
配置复杂度 相对简单,易于上手 相对复杂,需要花费更多时间来配置
IDE集成 广泛支持,包括PHPStorm、VS Code等 广泛支持,包括PHPStorm、VS Code等
泛型支持 相对有限 优秀,对泛型支持非常强大
社区活跃度 非常活跃 非常活跃
错误信息可读性 良好,能够提供清晰的错误信息和建议 良好,但有时可能不如PHPStan清晰

选择哪个工具取决于你的项目需求和偏好。 如果你希望快速上手并获得良好的性能,PHPStan可能是一个更好的选择。 如果你需要更强大的类型推断和泛型支持,Psalm可能更适合你。 许多项目也会同时使用这两个工具,以获得更全面的代码分析。

配置PHPStan的严格模式

PHPStan的严格程度通过level参数进行配置,范围从0到9。 level 0是最宽松的,只检查最基本的错误,而level 9是最严格的,会报告几乎所有潜在的问题。

可以通过在phpstan.neon文件中设置level参数来配置PHPStan的严格模式。 例如,要将PHPStan设置为level 7,可以这样做:

parameters:
    level: 7

随着项目的发展,可以逐步提高level,以逐步提高代码质量。 建议从较低的level开始,修复发现的问题,然后逐步提高level,直到达到一个平衡点,即代码质量和开发效率之间的最佳折衷。

除了level参数,还可以通过在phpstan.neon文件中设置其他参数来进一步定制PHPStan的行为。 例如,可以使用excludePaths参数排除某些目录或文件,或者使用ignoreErrors参数忽略特定的错误。

parameters:
    level: 7
    excludePaths:
        - tests
        - vendor
    ignoreErrors:
        - '#Call to an undefined method#'

配置Psalm的严格模式

Psalm的严格模式配置相对复杂,但提供了更多的灵活性。 Psalm使用level属性来控制严格程度,范围从1到8,1是最宽松的,8是最严格的。 此外,Psalm还使用config.xsd文件来定义配置选项,允许更精细的控制。

可以在psalm.xml文件中设置level属性来配置Psalm的严格模式。 例如,要将Psalm设置为level 5,可以这样做:

<?xml version="1.0"?>
<psalm
    errorLevel="5"
    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"/>
    </projectFiles>
</psalm>

与PHPStan类似,也可以使用excludeFilesignoreErrors元素来排除文件或忽略特定的错误。

<?xml version="1.0"?>
<psalm
    errorLevel="5"
    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"/>
    </projectFiles>

    <excludeFiles>
        <file name="src/SomeLegacyFile.php"/>
    </excludeFiles>

    <issueHandlers>
        <MissingReturnType>
            <errorLevel type="suppress">
                <referencedClass name="SomeLegacyClass"/>
            </errorLevel>
        </MissingReturnType>
    </issueHandlers>
</psalm>

Psalm的baseline功能也十分强大。 可以通过生成baseline文件,将现有代码中的错误标记为已知错误,然后专注于修复新引入的错误。 这对于大型项目来说非常有用,可以逐步提高代码质量,而不会被大量的现有错误所淹没。

开发自定义规则:PHPStan

PHPStan允许通过开发自定义规则来扩展其功能,以满足特定的项目需求。 自定义规则可以检测到PHPStan内置规则无法检测到的特定错误或代码风格问题。

要开发PHPStan自定义规则,需要创建一个类,实现PHPStanRulesRule接口。 这个接口定义了一个getNodeType()方法,用于指定规则应该应用于哪些类型的AST节点,以及一个processNode()方法,用于处理这些节点并报告错误。

例如,假设我们需要创建一个规则,禁止使用die()函数。 可以这样做:

<?php

namespace MyProjectPHPStanRules;

use PhpParserNode;
use PhpParserNodeExprExit_;
use PHPStanAnalyserScope;
use PHPStanRulesRule;

class NoDieRule implements Rule
{
    public function getNodeType(): string
    {
        return Exit_::class;
    }

    /**
     * @param Exit_ $node
     * @param Scope $scope
     * @return array<string>
     */
    public function processNode(Node $node, Scope $scope): array
    {
        if ($node->die) {
            return [
                '使用 die() 函数是不允许的,请使用异常或其他方式来处理错误。',
            ];
        }

        return [];
    }
}

在这个例子中,NoDieRule类实现了Rule接口,并指定它应该应用于Exit_类型的节点(die()exit()函数的AST节点)。 processNode()方法检查节点是否是die()函数,如果是,则报告一个错误。

要注册自定义规则,需要在phpstan.neon文件中添加一个services配置:

services:
    -
        class: MyProjectPHPStanRulesNoDieRule
        tags:
            - phpstan.rules.rule

开发自定义规则:Psalm

Psalm也允许通过插件来扩展其功能。 Psalm插件可以定义自定义类型、规则和代码转换。

要开发Psalm插件,需要创建一个类,实现PsalmPluginPluginEntryPointInterface接口。 这个接口定义了一个__invoke()方法,用于注册插件提供的功能。

例如,假设我们需要创建一个规则,禁止使用var_dump()函数。 可以这样做:

<?php

namespace MyProjectPsalmPlugin;

use PsalmPluginPluginEntryPointInterface;
use PsalmPluginRegistrationInterface;

class NoVarDumpPlugin implements PluginEntryPointInterface
{
    /**
     * @param RegistrationInterface $registration
     * @return void
     */
    public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void
    {
        $registration->registerHooksFromClass(NoVarDumpHook::class);
    }
}

这个插件注册了一个名为NoVarDumpHook的hook。 hook是一个可以拦截Psalm执行过程并执行自定义逻辑的类。

NoVarDumpHook类需要实现PsalmPluginHookAfterStatementAnalysisInterface接口,并实现afterStatementAnalysis()方法。 这个方法在每个语句分析之后被调用,可以在这里检查语句是否包含var_dump()函数,并报告错误。

<?php

namespace MyProjectPsalmPlugin;

use PhpParserNodeExprFuncCall;
use PsalmCodebase;
use PsalmCodeLocation;
use PsalmContext;
use PsalmIssuePluginIssue;
use PsalmIssuesSet;
use PsalmPluginHookAfterStatementAnalysisInterface;
use PsalmStatementsSource;

class NoVarDumpHook implements AfterStatementAnalysisInterface
{
    /**
     * @param FuncCall $stmt
     * @param Context $context
     * @param Codebase $codebase
     * @param IssuesSet $issues
     *
     * @return void
     */
    public static function afterStatementAnalysis(
        PhpParserNodeStmt $stmt,
        Context $context,
        Codebase $codebase,
        IssuesSet $issues,
        StatementsSource $source = null
    ) : void {
        if ($stmt instanceof FuncCall
            && $stmt->name instanceof PhpParserNodeName
            && strtolower($stmt->name->parts[0]) === 'var_dump'
        ) {
            $issues->add(
                new PluginIssue(
                    'NoVarDump',
                    '使用 var_dump() 函数是不允许的,请使用日志或其他方式来调试。',
                    new CodeLocation($source, $stmt)
                ),
                $source->getSuppressedIssues()
            );
        }
    }
}

要注册Psalm插件,需要在psalm.xml文件中添加一个plugins元素:

<?xml version="1.0"?>
<psalm
    errorLevel="5"
    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"/>
    </projectFiles>

    <plugins>
        <pluginClass class="MyProjectPsalmPluginNoVarDumpPlugin"/>
    </plugins>
</psalm>

最佳实践

  • 逐步提高严格程度: 不要一开始就将PHPStan或Psalm设置为最高级别,而是应该逐步提高严格程度,并修复发现的问题。
  • 使用Baseline: 使用Psalm的baseline功能,将现有代码中的错误标记为已知错误,然后专注于修复新引入的错误。
  • 编写清晰的错误信息: 在开发自定义规则时,确保编写清晰的错误信息,以便开发者能够理解问题并进行修复。
  • 编写单元测试: 为自定义规则编写单元测试,以确保它们能够正确地检测到目标错误。
  • 持续集成: 将静态分析集成到持续集成流程中,以便在每次代码提交时自动运行静态分析。
  • 团队协作: 确保团队成员都了解静态分析工具的配置和使用方法,并共同维护规则和配置。

总结

今天我们学习了如何配置PHPStan和Psalm的严格模式,以及如何开发自定义规则。 通过合理配置和扩展静态分析工具,可以显著提高PHP代码的质量和可维护性,减少运行时错误,并提升开发效率。 静态分析是现代PHP开发流程中不可或缺的一部分。

更精细的控制是关键

静态分析工具的强大之处在于其可配置性和可扩展性。 通过精细的配置和自定义规则,可以使其更好地适应项目的特定需求,从而最大限度地发挥其价值。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注