pepakriz / phpstan-exception-rules
PHPStan 的异常规则
v0.12.0
2021-11-07 19:03 UTC
Requires
- php: >=7.1
- nikic/php-parser: ^4.13
- phpstan/phpstan: ^1.0
Requires (Dev)
- nette/utils: ^3.0
- php-parallel-lint/php-console-highlighter: ^0.4.0
- php-parallel-lint/php-parallel-lint: ^1.2.0
- phpstan/phpstan-nette: ^1.0
- phpstan/phpstan-phpunit: ^1.0
- phpstan/phpstan-strict-rules: ^1.0
- phpunit/phpunit: ^7.5.6 || ^9.4.2
- slevomat/coding-standard: ^6.4.1
- squizlabs/php_codesniffer: ~3.5.2
- dev-master / 0.12.x-dev
- v0.12.0
- v0.11.7
- v0.11.6
- v0.11.5
- v0.11.4
- v0.11.3
- v0.11.2
- v0.11.1
- v0.11.0
- v0.10.1
- v0.10.0
- v0.9.0
- v0.8.3
- v0.8.2
- v0.8.1
- v0.8.0
- v0.7.2
- v0.7.1
- v0.7.0
- v0.6.3
- v0.6.2
- v0.6.1
- v0.6.0
- v0.5.0
- v0.4.4
- v0.4.3
- v0.4.2
- v0.4.1
- v0.4.0
- v0.3.3
- v0.3.2
- v0.3.1
- v0.3.0
- v0.2.0
- v0.1.1
- v0.1.0
- dev-VincentLanglet-patch-1
- dev-pk/remove-php71-support
- dev-master-build-fix
- dev-php-7.3-support
- dev-scope
- dev-catch-and-throw
This package is auto-updated.
Last update: 2024-09-12 16:24:17 UTC
README
此扩展提供以下规则和功能
- 在抛出某些检查异常时需要
@throws
注释(示例) - 异常传播
- 忽略捕获的检查异常(示例)
- 不必要的
@throws
注释检测(示例) - 无用
@throws
注释检测(示例) - 可选地允许子类型中未使用的
@throws
注释(示例) @throws
注释方差验证(示例)- 基于参数的动态抛出类型
- 不可达的捕获语句
- 在全局作用域中报告抛出检查异常(示例)
PHPStan 核心提供的特性和规则(我们依赖于此)
@throws
注释必须只包含有效的Throwable
类型- 抛出的值必须是
Throwable
的子类
用法
要使用此扩展,请在 Composer 中引入它
composer require --dev pepakriz/phpstan-exception-rules
并在您项目的 PHPStan 配置中包含和配置 extension.neon
includes: - vendor/pepakriz/phpstan-exception-rules/extension.neon parameters: exceptionRules: reportUnusedCatchesOfUncheckedExceptions: false reportUnusedCheckedThrowsInSubtypes: false reportCheckedThrowsInGlobalScope: false checkedExceptions: - RuntimeException
当您更愿意使用未检查的异常列表时,可以使用 uncheckedExceptions
。这是一个更安全的变体,但更难适应现有项目。
parameters: exceptionRules: uncheckedExceptions: - LogicException - PHPUnit\Framework\Exception
checkedExceptions
和uncheckedExceptions
不能同时配置
如果某些第三方代码定义了错误的抛出类型(或者根本不使用 @throws 注释),您可以像这样覆盖定义
parameters: exceptionRules: methodThrowTypeDeclarations: FooProject\SomeService: sendMessage: - FooProject\ConnectionTimeoutException methodWithoutException: [] functionThrowTypeDeclarations: myFooFunction: - FooException
在某些情况下,您可能希望根据类的基础忽略与异常相关的错误,这在测试中通常是这种情况
parameters: exceptionRules: methodWhitelist: PHPUnit\Framework\TestCase: '#^(test|(setup|setupbeforeclass|teardown|teardownafterclass)$)#i'
可扩展性
动态抛出类型扩展
- 如果抛出类型不是始终相同,而是依赖于传递给方法的参数。(与 动态返回类型扩展 类似的功能)
有一些接口,您可以实现
Pepakriz\PHPStanExceptionRules\DynamicMethodThrowTypeExtension
- 服务标签:exceptionRules.dynamicMethodThrowTypeExtension
Pepakriz\PHPStanExceptionRules\DynamicStaticMethodThrowTypeExtension
- 服务标签:exceptionRules.dynamicStaticMethodThrowTypeExtension
Pepakriz\PHPStanExceptionRules\DynamicConstructorThrowTypeExtension
- 服务标签:exceptionRules.dynamicConstructorThrowTypeExtension
Pepakriz\PHPStanExceptionRules\DynamicFunctionThrowTypeExtension
- 服务标签:exceptionRules.dynamicFunctionThrowTypeExtension
并将它们注册为具有正确标签的服务
services: - class: App\PHPStan\EntityManagerDynamicMethodThrowTypeExtension tags: - exceptionRules.dynamicMethodThrowTypeExtension
动机
有两种类型的异常
- 安全检查,表示某些事情永远不应该发生(例如,你永远不应该在某些情况下调用某些方法等)。我们称这些为 LogicException,如果它们被抛出,则程序员犯了错误。因此,重要的是这个异常永远不要被捕获并终止应用程序。同样,编写良好的描述性消息以说明发生了什么以及如何修复它也很重要——这就是为什么每个 LogicException 都必须有一个消息。因此,继承 LogicException 没有多少意义。LogicException 也永远不应该有
@throws
注解(见下文)。 - 业务逻辑中的特殊情况,应由应用程序处理,以及无论如何努力都可能发生错误的情况(例如,HTTP 请求可能会失败)。我们称这些为 RuntimeException 或更好的“已检查异常”。所有这些异常都应该进行检查。因此,它必须要么被捕获,要么在
@throws
注解中编写。此外,如果您调用具有该注解的方法并且没有捕获异常,您必须在您的@throws
注解中传播它。这当然可能会迅速传播。当此异常被处理(捕获)时,程序员立即知道处理了哪种情况非常重要,因此所有使用的 RuntimeException 都继承自某个父类,并且具有非常描述性的类名(因此您可以在捕获结构中看到它)——例如CannotCloseAccountWithPositiveBalanceException
。消息并不那么重要,因为您应该在某个地方始终捕获这些异常,但在我们的情况下,我们经常在 API 输出中使用该消息并将其显示给最终用户,因此请在这种情况下使用对用户有用的信息(您可以通过构造函数传递自定义参数(例如实体)来提供更好的消息)。有时您会遇到知道某些异常永远不会被抛出的地方——在这种情况下,您可以捕获它并将其包装到 LogicException 中(因为当它被抛出时,这是程序员的错误)。
始终将上一个异常包装起来是一个好主意,这样我们就不至于在日志中丢失真正发生的事情的信息。
// no throws annotation public function decide(int $arg): void { switch ($arg) { case self::ONE: $this->decided() case self::TWO: $this->decidedDifferently() default: throw new LogicException("Decision cannot be made for argument $arg because of ..."); } } /** * @return mixed[] * * @throws PrintJobFailedException */ private function sendRequest(Request $request): array { try { $response = $this->httpClient->send($request); return Json::decode((string) $response->getBody(), Json::FORCE_ARRAY); } catch (GuzzleException | JsonException $e) { throw new PrintJobFailedException($e); } } class PrintJobFailedException extends RuntimeException { public function __construct(Throwable $previous) { parent::__construct('Printing failed, remote printing service is down. Please try again later', $previous); } }
已知限制
匿名函数在声明的地方进行分析
如果方法没有执行声明的函数,则会产生误报
/** * @throws FooRuntimeException false positive */ public function createFnFoo(int $arg): callable { return function () { throw new FooRuntimeException(); }; }
但大多数用例都可以正常工作
/** * @param string[] $rows * @return string[] * * @throws EmptyLineException */ public function normalizeRows(array $rows): array { return array_map(function (string $row): string { $row = trim($row); if ($row === '') { throw new EmptyLineException(); } return $row; }, $rows); }
Catch
语句不了解运行时子类型
此情况由规则检测到,因此您将收到有关潜在风险的警告。
运行时异常被吸收
// @throws phpdoc is not required public function methodWithoutThrowsPhpDoc(): void { try { throw new RuntimeException(); $this->dangerousCall(); } catch (Throwable $e) { throw $e; } }
作为一种解决方案,您可以使用自定义的 catch
语句
/** * @throws RuntimeException */ public function methodWithThrowsPhpDoc(): void { try { throw new RuntimeException(); $this->dangerousCall(); } catch (RuntimeException $e) { throw $e; } catch (Throwable $e) { throw $e; } }