PHP代码的静态分析器(Psalm/PHPStan):集成到CI/CD流程与自定义规则配置

PHP代码静态分析:Psalm与PHPStan在CI/CD中的集成与自定义规则配置

各位朋友,大家好!今天我们来聊聊PHP静态分析工具Psalm和PHPStan,以及如何将它们集成到CI/CD流程中,并配置自定义规则,以提高代码质量和可维护性。

静态分析的意义

在软件开发过程中,越早发现问题,修复成本就越低。静态分析工具可以在不运行代码的情况下,通过分析代码结构、类型声明、代码风格等,发现潜在的错误、性能瓶颈和安全漏洞。这相当于在代码审查之前,就进行了一次“预审”,可以大大减少代码审查的工作量,提高开发效率,并降低线上故障的风险。

与动态分析(如单元测试、集成测试)相比,静态分析的优势在于:

  • 覆盖面广: 可以分析所有代码路径,而动态分析只能覆盖已执行的代码路径。
  • 自动化: 可以集成到CI/CD流程中,自动进行代码检查。
  • 快速反馈: 可以在开发阶段快速发现问题,避免问题蔓延到后续阶段。

Psalm与PHPStan:两款优秀的PHP静态分析工具

Psalm和PHPStan是目前PHP社区中比较流行的两款静态分析工具。它们都提供了强大的代码分析能力,可以帮助我们发现各种潜在问题。

  • Psalm: 侧重于类型检查,可以帮助我们发现类型错误、未定义变量、死代码等问题。Psalm的配置相对复杂,但可以提供更精确的类型推断。
  • PHPStan: 侧重于代码风格和潜在错误,可以帮助我们发现未使用变量、冗余代码、潜在的 null 指针解引用等问题。PHPStan的配置相对简单,上手容易。

两者并非互斥的关系,可以结合使用,以获得更全面的代码分析结果。

Psalm的使用示例

# 安装Psalm
composer require vimeo/psalm --dev

# 初始化Psalm配置
./vendor/bin/psalm --init

# 分析代码
./vendor/bin/psalm

Psalm的配置文件psalm.xml可以进行详细的配置,例如指定分析目录、忽略特定文件或目录、配置类型检查级别等。

<?xml version="1.0"?>
<psalm
    errorLevel="1"
    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.xml"
>
    <projectFiles>
        <directory name="src"/>
        <ignoreFiles>
            <directory name="vendor"/>
        </ignoreFiles>
    </projectFiles>

    <issueHandlers>
        <MissingParamType errorLevel="suppress"/>
        <MissingReturnType errorLevel="suppress"/>
    </issueHandlers>
</psalm>

PHPStan的使用示例

# 安装PHPStan
composer require phpstan/phpstan --dev

# 初始化PHPStan配置
./vendor/bin/phpstan analyse --level 5 src

# 分析代码
./vendor/bin/phpstan analyse src

PHPStan的配置文件phpstan.neon可以进行详细的配置,例如指定分析目录、忽略特定文件或目录、配置检查级别等。

parameters:
    level: 5
    paths:
        - src
    excludePaths:
        - vendor
    ignoreErrors:
        - '#Missing parameter type hint for $[a-zA-Z0-9]+ in method#'
        - '#Missing return type hint for method#'

级别配置

两者都支持配置检查级别,级别越高,检查越严格,发现的问题越多,但误报的可能性也越大。建议从较低的级别开始,逐步提高级别,并根据实际情况调整配置。

工具 级别范围 描述
Psalm 1-8 1是最严格的级别,会报告几乎所有类型错误和潜在问题;8是最宽松的级别,只会报告最严重的错误。
PHPStan 0-9 0是最宽松的级别,只会检查基本语法错误;9是最严格的级别,会检查所有可能的类型错误和潜在问题。

集成到CI/CD流程

将Psalm和PHPStan集成到CI/CD流程中,可以实现代码的自动检查,确保每次提交的代码都符合规范,并及时发现潜在问题。

以下是一个使用GitHub Actions集成Psalm和PHPStan的示例:

name: Static Analysis

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  static-analysis:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'
          extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, gd, intl

      - name: Install Dependencies
        run: composer install --no-interaction --no-progress --prefer-dist

      - name: Run Psalm
        run: ./vendor/bin/psalm

      - name: Run PHPStan
        run: ./vendor/bin/phpstan analyse src

这个GitHub Actions的工作流程包含以下步骤:

  1. 检出代码: 使用actions/checkout@v3检出代码。
  2. 设置PHP环境: 使用shivammathur/setup-php@v2设置PHP环境,并安装所需的扩展。
  3. 安装依赖: 使用composer install安装依赖。
  4. 运行Psalm: 使用./vendor/bin/psalm运行Psalm。
  5. 运行PHPStan: 使用./vendor/bin/phpstan analyse src运行PHPStan。

如果Psalm或PHPStan发现任何问题,工作流程将失败,阻止代码合并到主分支。

集成流程优化

为了提高CI/CD流程的效率,可以进行以下优化:

  • 缓存依赖: 使用GitHub Actions的缓存功能,缓存Composer依赖,避免每次构建都重新安装依赖。
  • 并行运行: 如果代码库较大,可以并行运行Psalm和PHPStan,缩短构建时间。
  • 增量分析: Psalm和PHPStan都支持增量分析,只分析修改过的文件,可以大大提高分析速度。

自定义规则配置

Psalm和PHPStan都支持自定义规则,可以根据项目的特定需求,定制代码检查规则。

Psalm的自定义插件

Psalm支持自定义插件,可以编写PHP代码来实现自定义规则。

以下是一个自定义Psalm插件的示例,用于检查类名是否符合命名规范:

<?php

namespace MyProjectPsalm;

use PsalmPluginPluginEntryPointInterface;
use PsalmPluginRegistrationInterface;

class MyCustomPlugin implements PluginEntryPointInterface
{
    public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void
    {
        $registration->registerHooksFromClass(ClassNameChecker::class);
    }
}
<?php

namespace MyProjectPsalm;

use PsalmCodebase;
use PsalmIssuePluginIssue;
use PsalmIssueBuffer;
use PsalmPluginHookAfterClassLikeVisitInterface;
use PhpParserNodeStmtClassLike;

class ClassNameChecker implements AfterClassLikeVisitInterface
{
    /**
     * Called after Psalm has finished visiting a class-like node.
     *
     * @param  ClassLike  $stmt
     * @param  string|null  $fq_classlike_name
     * @param  Codebase  $codebase
     *
     * @return void
     */
    public static function afterClassLikeVisit(ClassLike $stmt, ?string $fq_classlike_name, Codebase $codebase): void
    {
        if ($fq_classlike_name === null) {
            return;
        }

        if (!preg_match('/^[A-Z][a-zA-Z0-9]+$/', $stmt->name->name)) {
            if (IssueBuffer::accepts(
                new PluginIssue(
                    'InvalidClassName',
                    'Class name ' . $stmt->name->name . ' does not match the naming convention.',
                    $stmt->getStartLine(),
                    $stmt->getEndLine()
                ),
                $codebase->getAbsolutePath($stmt->getStartFilepath()),
                'InvalidClassName'
            )) {
                // suppression might have blocked it
            }
        }
    }
}

需要在psalm.xml中注册插件:

<?xml version="1.0"?>
<psalm
    errorLevel="1"
    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.xml"
>
    <projectFiles>
        <directory name="src"/>
        <ignoreFiles>
            <directory name="vendor"/>
        </ignoreFiles>
    </projectFiles>

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

PHPStan的自定义规则

PHPStan支持自定义规则,可以编写PHP代码来实现自定义规则。

以下是一个自定义PHPStan规则的示例,用于检查类名是否符合命名规范:

<?php

namespace MyProjectPHPStan;

use PhpParserNode;
use PhpParserNodeStmtClass_;
use PHPStanAnalyserScope;
use PHPStanRulesRule;

/**
 * @implements Rule<Class_>
 */
class ClassNameRule implements Rule
{
    public function getNodeType(): string
    {
        return Class_::class;
    }

    public function processNode(Node $node, Scope $scope): array
    {
        if (!preg_match('/^[A-Z][a-zA-Z0-9]+$/', $node->name->name)) {
            return [
                sprintf('Class name %s does not match the naming convention.', $node->name->name),
            ];
        }

        return [];
    }
}

需要在phpstan.neon中注册规则:

parameters:
    level: 5
    paths:
        - src
    excludePaths:
        - vendor
    ignoreErrors:
        - '#Missing parameter type hint for $[a-zA-Z0-9]+ in method#'
        - '#Missing return type hint for method#'
    rules:
        - MyProjectPHPStanClassNameRule

自定义规则的最佳实践

  • 规则应该简单明了: 避免编写过于复杂的规则,难以理解和维护。
  • 规则应该具有针对性: 针对项目的特定需求,定制规则。
  • 规则应该经过充分测试: 确保规则能够正确地发现问题,并且不会产生误报。

实际案例分析

假设我们有一个电商项目,需要确保所有价格都以小数点后两位显示。我们可以编写一个自定义规则来检查代码中是否存在价格格式不正确的场景。

Psalm自定义规则:

<?php

namespace MyProjectPsalm;

use PsalmCodebase;
use PsalmIssuePluginIssue;
use PsalmIssueBuffer;
use PsalmPluginHookAfterExpressionAnalysisInterface;
use PhpParserNodeExpr;
use PhpParserNodeScalarString_;

class PriceFormatChecker implements AfterExpressionAnalysisInterface
{
    public static function afterExpressionAnalysis(
        Expr $expr,
        PsalmContext $context,
        Codebase $codebase,
        array &$file_replacements = []
    ): ?bool {
        if ($expr instanceof String_) {
            if (preg_match('/^d+(.d{1,2})?$/', $expr->value) && strpos($expr->value, '.') !== false) {
                if (preg_match('/^d+.d{1}$/', $expr->value)) {
                    if (IssueBuffer::accepts(
                        new PluginIssue(
                            'InvalidPriceFormat',
                            'Price format ' . $expr->value . ' should have two decimal places.',
                            $expr->getStartLine(),
                            $expr->getEndLine()
                        ),
                        $codebase->getAbsolutePath($expr->getStartFilepath()),
                        'InvalidPriceFormat'
                    )) {
                        // suppression might have blocked it
                    }
                }
            }
        }

        return null;
    }
}

PHPStan自定义规则:

<?php

namespace MyProjectPHPStan;

use PhpParserNode;
use PhpParserNodeScalarString_;
use PHPStanAnalyserScope;
use PHPStanRulesRule;

/**
 * @implements Rule<String_>
 */
class PriceFormatRule implements Rule
{
    public function getNodeType(): string
    {
        return String_::class;
    }

    public function processNode(Node $node, Scope $scope): array
    {
        if (preg_match('/^d+(.d{1,2})?$/', $node->value) && strpos($node->value, '.') !== false) {
            if (preg_match('/^d+.d{1}$/', $node->value)) {
                return [
                    sprintf('Price format %s should have two decimal places.', $node->value),
                ];
            }
        }

        return [];
    }
}

通过这个自定义规则,我们可以确保所有价格都以小数点后两位显示,提高用户体验。

结论

Psalm和PHPStan是强大的PHP静态分析工具,可以帮助我们发现潜在的错误、性能瓶颈和安全漏洞。将它们集成到CI/CD流程中,并配置自定义规则,可以提高代码质量和可维护性,降低线上故障的风险。

几句话来总结

  • 静态分析是提高代码质量的有效手段,Psalm和PHPStan是优秀的PHP静态分析工具。
  • 将静态分析集成到CI/CD流程中,可以实现代码的自动检查。
  • 自定义规则可以根据项目的特定需求,定制代码检查规则。

发表回复

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