PHP `PHPStan` / `Psalm` (静态分析工具) 自定义规则与插件开发

PHP静态分析:解锁你的代码超能力,从自定义规则开始!

大家好!我是你们今天的代码向导,今天要带大家深入PHP静态分析的世界,特别是如何利用PHPStan和Psalm编写自定义规则和插件,让你的代码质量更上一层楼。

先跟大家打个招呼,今天的旅程略微烧脑,但只要跟着我的节奏,保证你能收获满满,到时候写出让同事惊艳,让bug绕道的代码!

静态分析:代码侦探的秘密武器

首先,咱们来聊聊啥是静态分析。简单来说,静态分析就像一个代码侦探,它不用真正运行你的代码,就能找出潜在的问题,比如:

  • 类型错误: 比如你试图把一个字符串当成数字用。
  • 未使用的变量: 你定义了一个变量,结果压根没用上。
  • 潜在的NullPointerException (虽然PHP里叫TypeError): 你访问了一个可能为null的变量的属性。
  • 代码风格问题: 比如命名不规范,代码过于复杂等等。

与动态分析(运行代码然后测试)相比,静态分析的优势在于:

  • 更早发现问题: 避免问题上线才暴露,减少修复成本。
  • 覆盖更全面: 静态分析可以检查所有可能的代码路径,而测试通常只能覆盖部分。
  • 代码质量提升: 帮助你写出更清晰、更易于维护的代码。

PHP世界里,PHPStan和Psalm是两款非常流行的静态分析工具。它们都非常强大,各有特点,大家可以根据自己的需求选择。

PHPStan:严格的守门员

PHPStan以其严格性著称,它会尽力找出代码中的每一个潜在问题。想象一下,它就像一个非常严格的守门员,不放过任何一个可疑的球。

Psalm:灵活的侦察兵

Psalm则更加灵活,它允许你自定义分析的严格程度,并且提供了更丰富的类型推断功能。它就像一个经验丰富的侦察兵,能够深入代码的每一个角落,发现隐藏的风险。

准备工作:磨刀不误砍柴工

在开始编写自定义规则之前,我们需要先准备好环境。

  1. 安装PHPStan或Psalm:

    使用Composer安装:

    composer require --dev phpstan/phpstan
    # 或者
    composer require --dev vimeo/psalm
  2. 配置:

    创建phpstan.neonpsalm.xml文件,配置需要分析的文件和目录。

    一个简单的phpstan.neon 示例:

    parameters:
        level: 7 # 分析级别,数值越高越严格
        paths:
            - src # 需要分析的目录
            - tests # 测试目录
        excludePaths:
            - vendor # 排除vendor目录

    一个简单的psalm.xml 示例:

    <?xml version="1.0"?>
    <psalm
        errorLevel="2"
        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"/>
            <directory name="tests"/>
            <ignoreFiles>
                <directory name="vendor"/>
            </ignoreFiles>
        </projectFiles>
    </psalm>
  3. 运行:

    在命令行中运行:

    ./vendor/bin/phpstan analyse src
    # 或者
    ./vendor/bin/psalm

自定义规则:让静态分析更懂你的代码

现在,激动人心的时刻到了!让我们开始编写自定义规则,让静态分析工具更懂你的代码。

场景:禁止使用die()exit()

在实际项目中,我们通常不希望直接使用die()exit(),因为它们会立即终止脚本的执行,不利于错误处理和程序控制。所以,我们需要一个规则来禁止使用它们。

PHPStan自定义规则

  1. 创建规则类:

    创建一个类,实现PHPStanRulesRule接口。

    <?php
    
    namespace MyRules;
    
    use PhpParserNode;
    use PhpParserNodeExprExit_;
    use PHPStanAnalyserScope;
    use PHPStanRulesRule;
    
    class NoDieOrExitRule implements Rule
    {
        public function getNodeType(): string
        {
            return Exit_::class;
        }
    
        public function processNode(Node $node, Scope $scope): array
        {
            /** @var Exit_ $node */
            return [
                '不要使用 die() 或 exit(),请使用异常或其他方式处理。',
            ];
        }
    }

    解释一下:

    • getNodeType():指定这个规则要处理的节点类型,这里是Exit_::class,表示die()exit()
    • processNode():处理节点的逻辑,如果发现使用了die()exit(),就返回一个错误信息。
  2. 注册规则:

    phpstan.neon文件中注册这个规则。

    parameters:
        level: 7
        paths:
            - src
        rules:
            - MyRulesNoDieOrExitRule
  3. 运行PHPStan:

    运行./vendor/bin/phpstan analyse src,如果你的代码中使用了die()exit(),就会看到错误信息。

Psalm自定义规则

  1. 创建插件类:

    创建一个类,实现PsalmPluginPluginEntryPointInterface接口。

    <?php
    
    namespace MyRules;
    
    use PsalmPluginPluginEntryPointInterface;
    use PsalmPluginRegistrationInterface;
    
    class MyPsalmPlugin implements PluginEntryPointInterface
    {
        public function __invoke(RegistrationInterface $api, ?SimpleXMLElement $config = null): void
        {
            $api->registerHooksFromClass(NoDieOrExitAnalyzer::class);
        }
    }
  2. 创建分析器类:

    创建一个类,实现PsalmPluginHookAfterEveryFunctionCallHookInterface接口。

    <?php
    
    namespace MyRules;
    
    use PhpParserNodeExprFuncCall;
    use PsalmCodebase;
    use PsalmCodeLocation;
    use PsalmContext;
    use PsalmIssuePluginIssue;
    use PsalmPluginHookAfterEveryFunctionCallHookInterface;
    use PsalmStatementsSource;
    use PsalmType;
    
    class NoDieOrExitAnalyzer implements AfterEveryFunctionCallHookInterface
    {
        /**
         * @param FuncCall $expr
         * @param Context $context
         * @param CodeLocation $code_location
         *
         * @return null|false
         */
        public static function afterEveryFunctionCall(
            FuncCall $expr,
            string $function_id,
            array $resolved_call_args,
            TypeUnion $return_type_candidate,
            Context $context,
            StatementsSource $statements_source,
            Codebase $codebase,
            array &$file_replacements = [],
            CodeLocation $code_location = null
        ) : ?bool {
            if (in_array($function_id, ['die', 'exit'], true)) {
                $statements_source->addIssue(
                    new PluginIssue(
                        'NoDieOrExit',
                        '不要使用 die() 或 exit(),请使用异常或其他方式处理。',
                        $code_location ?? new CodeLocation($statements_source, $expr)
                    )
                );
            }
    
            return null;
        }
    }

    解释一下:

    • MyPsalmPlugin是插件入口,负责注册hook。
    • NoDieOrExitAnalyzer实现了AfterEveryFunctionCallHookInterface接口,会在每次函数调用后被调用。
    • afterEveryFunctionCall()方法会检查调用的函数是否是die()exit(),如果是,就添加一个issue。
  3. 注册插件:

    psalm.xml文件中注册这个插件。

    <?xml version="1.0"?>
    <psalm
        errorLevel="2"
        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"/>
            <directory name="tests"/>
            <ignoreFiles>
                <directory name="vendor"/>
            </ignoreFiles>
        </projectFiles>
    
        <plugins>
            <pluginClass class="MyRulesMyPsalmPlugin"/>
        </plugins>
    </psalm>
  4. 运行Psalm:

    运行./vendor/bin/psalm,如果你的代码中使用了die()exit(),就会看到错误信息。

自定义规则进阶:更多可能性

上面的例子只是一个简单的入门,自定义规则的可能性是无限的。你可以根据自己的需求,编写更复杂的规则,比如:

  • 检查代码复杂度: 避免函数过于冗长,提高代码可读性。
  • 强制使用特定的设计模式: 确保代码结构符合规范。
  • 防止SQL注入: 检查SQL语句是否经过安全处理。
  • 检查API调用是否正确: 确保API调用的参数和返回值符合预期。

一些常用的接口和类

工具 接口/类 说明
PHPStan PHPStanRulesRule 定义一个规则
PHPStan PhpParserNode 表示代码中的一个节点,比如一个变量、一个表达式、一个函数调用等等
PHPStan PHPStanAnalyserScope 提供代码的上下文信息,比如当前类、当前方法、变量类型等等
Psalm PsalmPluginPluginEntryPointInterface 定义一个插件的入口
Psalm PsalmPluginRegistrationInterface 用于注册hook
Psalm PsalmPluginHook* 定义各种类型的hook,比如在函数调用前后、类定义前后等等
Psalm PsalmCodebase 提供代码库的信息,比如类、函数、常量等等
Psalm PsalmContext 提供代码的上下文信息,比如当前类、当前方法、变量类型等等
Psalm PsalmIssuePluginIssue 用于报告一个插件发现的问题

测试你的规则:确保万无一失

编写完自定义规则后,一定要进行测试,确保它能够正确地工作。

  1. 创建测试用例:

    编写一些包含符合规则和违反规则的代码的测试用例。

  2. 运行测试:

    使用PHPUnit或其他测试框架运行测试用例,检查规则是否能够正确地识别出问题。

分享你的规则:让更多人受益

如果你编写了一些非常有用的自定义规则,不妨分享出来,让更多人受益。

  1. 创建开源项目:

    将你的规则发布到GitHub或其他代码托管平台上。

  2. 编写文档:

    提供清晰的文档,说明如何安装和使用你的规则。

  3. 积极维护:

    及时修复bug,并根据用户的反馈进行改进。

总结:代码质量的守护者

通过自定义规则和插件,我们可以让PHPStan和Psalm更好地理解我们的代码,从而提高代码质量,减少bug,让我们的项目更加健壮。

希望今天的讲座能够帮助大家解锁代码超能力,成为代码质量的守护者!下次再见!

发表回复

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