静态分析器PHPStan的高级配置:L5/L6级别、自定义扩展与CI/CD集成

PHPStan 高级配置:L5/L6级别、自定义扩展与CI/CD集成

大家好!今天我们要深入探讨 PHPStan 的高级配置,涵盖 L5/L6 级别的优化、自定义扩展的开发以及与 CI/CD 流畅集成。PHPStan 是一款强大的静态分析工具,它可以帮助我们在不实际运行代码的情况下发现潜在的错误和性能问题。通过对其进行精细配置和扩展,我们可以显著提升代码质量,减少 bug 数量,并提高开发效率。

一、理解 PHPStan 分析级别:L0 到 L9

PHPStan 提供了一系列分析级别,从 L0 (最宽松) 到 L9 (最严格)。级别越高,检测的错误类型越多,但误报的可能性也相应增加。选择合适的级别是关键,需要在严格性和实用性之间找到平衡。

分析级别 描述 适用场景
L0 仅检查基本的语法错误和类型声明。 作为初始配置,快速发现最明显的错误。
L1-L4 逐步增加类型检查的严格性,例如检查变量是否已定义、参数类型是否匹配等。 适合逐步改进现有代码库,避免一次性引入大量错误报告。
L5-L6 专注于更复杂的类型推断,例如泛型类型、联合类型和交叉类型。开始检查一些潜在的性能问题。 适合代码库类型提示较为完善,希望进一步提高代码质量和性能的项目。
L7-L9 最严格的级别,会进行深度类型分析和复杂的控制流分析。可能会产生较多的误报,需要根据具体情况进行调整。 适合对代码质量要求极高的项目,或者在重构过程中使用,以确保代码的正确性。

二、L5/L6 级别配置:精细控制类型检查

L5 和 L6 级别是 PHPStan 中比较常用的高级别。它们在类型推断和错误检测方面做了大量工作,可以有效地发现潜在的 bug。

2.1 选择 L5/L6 的理由

  • 更精确的类型推断: L5/L6 级别能够更好地理解代码中的类型关系,例如通过 PHPDoc、返回值类型声明和参数类型声明等。
  • 检测更复杂的错误: 可以检测到一些只有在运行时才会出现的错误,例如访问未定义的属性、调用不存在的方法等。
  • 性能优化提示: L6 级别开始会检查一些性能问题,例如循环中的重复计算。

2.2 配置 phpstan.neon 文件

要配置 PHPStan 的分析级别,我们需要修改 phpstan.neon (或 phpstan.neon.dist) 文件。以下是一个示例配置:

parameters:
    level: 5 # 或 6
    paths:
        - src
        - tests
    excludePaths:
        - src/Migrations
    checkMissingIterableValueType: false # 避免对未指定 value 类型的 iterable 产生错误
    reportUnmatchedIgnoredErrors: false # 避免报告未被任何忽略规则匹配的错误
  • level: 指定分析级别。
  • paths: 指定要分析的代码目录。
  • excludePaths: 指定要排除的代码目录,例如数据库迁移文件。
  • checkMissingIterableValueType: 控制是否检查 iterable 类型是否缺少 value 类型声明。
  • reportUnmatchedIgnoredErrors: 控制是否报告未被任何 ignoreErrors 规则匹配的错误。

2.3 忽略特定错误

在 L5/L6 级别,可能会出现一些误报。我们可以使用 ignoreErrors 配置来忽略特定的错误。

parameters:
    level: 5
    paths:
        - src
    ignoreErrors:
        -
            message: '#Access to an undefined property#'
            path: src/LegacyCode.php # 忽略特定文件中的错误
        -
            message: '#Parameter #1 $value of method Foo::bar() expects string, int given.#'
            path: src # 忽略整个目录下的特定错误
        -
            message: '#Call to an undefined method App\\Model\\User::getProfile().#'
            count: 5 # 忽略特定错误出现 5 次以内的情况
  • message: 正则表达式,用于匹配错误信息。
  • path: 可选参数,指定要忽略错误的文件或目录。
  • count: 可选参数,忽略错误出现的次数,直到达到 count 的值才开始报告。

2.4 处理泛型类型

PHPStan 对泛型类型的支持在 L5/L6 级别得到了显著提升。我们可以使用 PHPDoc 来声明泛型类型。

<?php

/**
 * @template T
 */
class Collection
{
    /**
     * @var T[]
     */
    private array $items = [];

    /**
     * @param T $item
     */
    public function add($item): void
    {
        $this->items[] = $item;
    }

    /**
     * @return T[]
     */
    public function getItems(): array
    {
        return $this->items;
    }
}

$collection = new Collection<string>();
$collection->add("hello");
$collection->add(123); // PHPStan 会报错,因为期望的是 string 类型

在这个例子中,@template T 声明了一个泛型类型 T,并在 Collection 类的属性和方法中使用它。PHPStan 会根据泛型类型进行类型检查。

三、自定义扩展:增强 PHPStan 的能力

PHPStan 的强大之处在于其可扩展性。我们可以通过自定义扩展来添加新的规则、类型解析器和动态返回类型扩展,以满足特定的需求。

3.1 自定义规则 (Rules)

自定义规则允许我们编写自己的代码检查逻辑。例如,我们可以创建一个规则来禁止使用 die()exit() 函数。

3.1.1 创建规则类

<?php

namespace AppPHPStanRules;

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

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

    public function processNode(Node $node, Scope $scope): array
    {
        return [
            'Do not use die() or exit() functions.'
        ];
    }
}
  • getNodeType() 方法指定规则要处理的节点类型。
  • processNode() 方法接收一个节点和一个作用域,并返回一个包含错误信息的数组。

3.1.2 注册规则

我们需要在 phpstan.neon 文件中注册我们的规则。

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

3.2 自定义类型解析器 (Type Resolvers)

类型解析器允许我们自定义 PHPStan 如何解析类型。例如,我们可以创建一个类型解析器来处理一些自定义的类型注解。

3.3 动态返回类型扩展 (Dynamic Return Type Extensions)

动态返回类型扩展允许我们根据方法的参数值或其他条件来动态地确定方法的返回类型。例如,我们可以根据工厂方法的参数来确定返回对象的类型。

3.3.1 创建动态返回类型扩展类

<?php

namespace AppPHPStanExtensions;

use PhpParserNodeExprMethodCall;
use PHPStanAnalyserScope;
use PHPStanReflectionMethodReflection;
use PHPStanTypeDynamicMethodReturnTypeExtension;
use PHPStanTypeObjectType;
use PHPStanTypeType;

class FactoryReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
    public function getClass(): string
    {
        return 'AppFactory';
    }

    public function isMethodSupported(MethodReflection $methodReflection): bool
    {
        return $methodReflection->getName() === 'create';
    }

    public function getTypeFromMethodCall(
        MethodReflection $methodReflection,
        MethodCall $methodCall,
        Scope $scope
    ): ?Type {
        if (count($methodCall->args) === 0) {
            return null;
        }

        $argType = $scope->getType($methodCall->args[0]->value);

        if (!$argType->isString()->yes()) {
            return null;
        }

        $value = $argType->getValue();

        if ($value === null) {
            return null;
        }

        return new ObjectType('App\' . ucfirst($value));
    }
}
  • getClass() 方法指定要扩展的类名。
  • isMethodSupported() 方法指定要扩展的方法名。
  • getTypeFromMethodCall() 方法根据方法调用时的参数来确定返回类型。

3.3.2 注册动态返回类型扩展

services:
    -
        class: AppPHPStanExtensionsFactoryReturnTypeExtension
        tags:
            - phpstan.broker.dynamicMethodReturnTypeExtension

3.4 打包和发布扩展

可以将自定义扩展打包成 Composer 包,方便在不同的项目中共享和使用。需要在 composer.json 文件中指定扩展类的命名空间,并在 extra 字段中添加 phpstan.neon 文件的路径。

{
    "name": "your-vendor/phpstan-extension",
    "type": "phpstan-extension",
    "autoload": {
        "psr-4": {
            "YourVendor\PHPStanExtension\": "src/"
        }
    },
    "extra": {
        "phpstan": {
            "neon": "extension.neon"
        }
    }
}

四、CI/CD 集成:自动化代码质量检查

将 PHPStan 集成到 CI/CD 流程中可以实现代码质量的自动化检查,确保每次提交的代码都符合规范。

4.1 GitHub Actions 示例

以下是一个使用 GitHub Actions 运行 PHPStan 的示例:

name: PHPStan

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

jobs:
  phpstan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup 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
          coverage: none
      - name: Install Dependencies
        run: composer install --no-interaction --no-progress --prefer-dist
      - name: Run PHPStan
        run: vendor/bin/phpstan analyse --memory-limit=2G
  • 使用 actions/checkout@v3 检出代码。
  • 使用 shivammathur/setup-php@v2 安装 PHP。
  • 使用 composer install 安装依赖。
  • 使用 vendor/bin/phpstan analyse 运行 PHPStan。

4.2 GitLab CI 示例

以下是一个使用 GitLab CI 运行 PHPStan 的示例:

stages:
  - analyse

phpstan:
  image: php:8.1-cli
  stage: analyse
  before_script:
    - apt-get update -yq
    - apt-get install -yq --no-install-recommends git zip unzip
    - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
    - composer install --no-interaction --no-progress --prefer-dist
  script:
    - vendor/bin/phpstan analyse --memory-limit=2G

4.3 失败处理

如果 PHPStan 检测到错误,CI/CD 流程应该失败,以防止不符合规范的代码被合并到主分支。可以在 CI/CD 脚本中添加错误处理逻辑,例如:

vendor/bin/phpstan analyse --memory-limit=2G
if [ $? -ne 0 ]; then
  echo "PHPStan found errors. Please fix them."
  exit 1
fi

五、常见问题与最佳实践

5.1 性能问题

  • 使用缓存: PHPStan 会缓存分析结果,以提高性能。确保缓存目录可写,并定期清理缓存。
  • 排除不必要的目录: 排除不需要分析的目录,例如 vendor 目录。
  • 增加内存限制: 对于大型项目,可能需要增加 PHPStan 的内存限制。

5.2 误报处理

  • 仔细分析错误信息: 不要盲目地忽略错误。仔细分析错误信息,确定是否是真正的 bug。
  • 使用 ignoreErrors 配置: 如果确定是误报,可以使用 ignoreErrors 配置来忽略它。
  • 提交 issue: 如果认为 PHPStan 的规则有问题,可以提交 issue 给 PHPStan 团队。

5.3 逐步升级分析级别

不要一次性将分析级别升级到最高。逐步升级分析级别,并解决每个级别发现的错误。

5.4 保持代码清洁

  • 编写清晰的代码: 清晰的代码更容易被 PHPStan 分析,减少误报的可能性。
  • 添加类型提示: 类型提示可以帮助 PHPStan 更好地理解代码,提高类型推断的准确性。
  • 使用 PHPDoc: PHPDoc 可以提供额外的类型信息,帮助 PHPStan 分析代码。

六、持续改进代码质量

通过合理配置 PHPStan 的分析级别,开发自定义扩展,并将其集成到 CI/CD 流程中,可以有效地提高代码质量,减少 bug 数量,并提高开发效率。持续改进代码质量是一个长期过程,需要不断的学习和实践。

通过对静态分析工具的深度理解和灵活运用,我们可以构建更加健壮和可靠的软件系统。

希望今天的分享对大家有所帮助!

发表回复

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