shipmonk / phpstan-rules
我们在ShipMonk项目中找到的各种额外严格的PHPStan规则。
Requires
- php: ^7.4 || ^8.0
- phpstan/phpstan: ^1.11.0
Requires (Dev)
- editorconfig-checker/editorconfig-checker: ^10.6.0
- ergebnis/composer-normalize: ^2.28
- nette/neon: ^3.3.1
- phpstan/phpstan-phpunit: ^1.4.0
- phpstan/phpstan-strict-rules: ^1.6.0
- phpunit/phpunit: ^9.5.20
- shipmonk/composer-dependency-analyser: ^1.3.0
- shipmonk/name-collision-detector: ^2.0.0
- slevomat/coding-standard: ^8.0.1
- dev-master
- 3.2.0
- 3.1.0
- 3.0.0
- 2.12.0
- 2.11.3
- 2.11.2
- 2.11.1
- 2.11.0
- 2.10.2
- 2.10.1
- 2.10.0
- 2.9.2
- 2.9.1
- 2.9.0
- 2.8.0
- 2.7.0
- 2.6.3
- 2.6.2
- 2.6.1
- 2.6.0
- 2.5.1
- 2.5.0
- 2.4.1
- 2.4.0
- 2.3.1
- 2.3.0
- 2.2.0
- 2.1.1
- 2.1.0
- 2.0.1
- 2.0.0
- 1.2.0
- 1.1.1
- 1.1.0
- 1.0.1
- 1.0.0
- dev-bc-changes-v3
- dev-first-devonly-rule
- dev-tests-error-tags
- dev-native-return-narrow-revive
- dev-no-remember-function-results
This package is auto-updated.
Last update: 2024-09-10 18:44:06 UTC
README
关于 40个超级严格的规则,我们在ShipMonk中找到这些规则非常有用。我们倾向于将PHPStan设置得尽可能严格,但这仍然不足以满足我们的需求。这些规则应该能填补我们发现的空白。
如果您发现某些规则具有争议性,您可以轻松地禁用它们。
安装
composer require --dev shipmonk/phpstan-rules
使用官方扩展安装程序或手动启用所有规则:
# phpstan.neon includes: - vendor/shipmonk/phpstan-rules/rules.neon
配置
您可以轻松地禁用或重新配置任何规则,例如
parameters: shipmonkRules: enforceReadonlyPublicProperty: enabled: false forbidUnsafeArrayKey: reportMixed: false
或者您可以选择禁用所有规则,只启用您想要的规则
parameters: shipmonkRules: enableAllRules: false allowComparingOnlyComparableTypes: enabled: true
当您尝试配置任何默认数组时,PHPStan配置默认会进行合并,因此如果您只想强制使用您的值而不是包含我们的默认值,请使用感叹号
parameters: shipmonkRules: forbidCast: enabled: true blacklist!: ['(unset)'] # force the blacklist to be only (unset)
一些规则被启用,但除非配置,否则不做任何事情,这些规则用*
标记。
规则
allowComparingOnlyComparableTypes
- 拒绝在除
int|string|float|DateTimeInterface
或相同大小的包含可比较类型的元组之外使用比较运算符>,<,<=,>=,<=>
。不允许使用Null。 - 在这些运算符中混合不同类型也是不允许的,唯一的例外是比较浮点数和整数
- 主要针对在PHP中有效但非常棘手的对象、枚举或数组之间的意外比较
function example1(Money $fee1, Money $fee2) { if ($fee1 > $fee2) {} // comparing objects is denied } new DateTime() > '2040-01-02'; // comparing different types is denied 200 > '1e2'; // comparing different types is denied
backedEnumGenerics *
- 确保每个BackedEnum子类定义泛型类型
- 此规则仅在BackedEnum被像这篇文章中描述的那样通过存根设置为泛型时才有意义
- 如果BackedEnum未设置为泛型,则此规则不起作用,这是默认设置。使用以下配置真正开始使用它
parameters: stubFiles: - BackedEnum.php.stub # see article or BackedEnumGenericsRuleTest ignoreErrors: - '#^Enum .*? has @implements tag, but does not implement any interface.$#'
enum MyEnum: string { // missing @implements tag case MyCase = 'case1'; }
classSuffixNaming *
- 允许您强制配置超类的子类的类名后缀
- 默认情况下不进行检查,通过传递
superclass => suffix
映射来配置它 - 不期望传递的超类具有此类后缀,只有子类才具有
- 您可以使用接口作为超类
shipmonkRules: classSuffixNaming: superclassToSuffixMapping!: \Exception: Exception \PHPStan\Rules\Rule: Rule \PHPUnit\Framework\TestCase: Test \Symfony\Component\Console\Command\Command: Command
enforceClosureParamNativeTypehint
- 强制使用原生类型提示进行闭包和箭头函数参数
- PHP 7.4及以下版本不做任何事情,因为那里没有原生
mixed
- 可以通过设置
allowMissingTypeWhenInferred: true
来配置,以允许在可以从上下文中推断出类型提示时省略类型提示
/** * @param list<Entity> $entities * @return list<Uuid> */ public function getIds(array $entities): array { return array_map( function ($entity) { // missing native typehint; not reported with allowMissingTypeWhenInferred: true return $entity->id; }, $entities ); }
enforceEnumMatchRule
- 强制使用
match ($enum)
而不是像if ($enum === Enum::One) elseif ($enum === Enum::Two)
这样的穷举条件 - 此规则旨在“修复”PHPStan的一点点问题行为(从1.10.0版本引入,在1.10.34版本中修复:1.10.34)。它非常了解枚举情况,并强制您调整以下代码
enum MyEnum { case Foo; case Bar; } if ($enum === MyEnum::Foo) { // ... } elseif ($enum === MyEnum::Bar) { // always true reported by phpstan (for versions 1.10.0 - 1.10.34) // ... } else { throw new LogicException('Unknown case'); // phpstan knows it cannot happen }
有人可能会将其修复为
if ($enum === MyEnum::Foo) { // ... } elseif ($enum === MyEnum::Bar) { // ... }
或者更糟糕的是
if ($enum === MyEnum::Foo) { // ... } else { // ... }
我们认为这会导致代码更容易出错,因为添加新的枚举情况可能不会在测试中失败。在类似的情况下,使用match
构造是一个非常好的方法(理想情况下启用forbidMatchDefaultArmForEnums
),这样PHPStan一旦添加新情况就会失败。从1.10.11
版本开始,PHPStan甚至在那些情况下添加了关于match
的提示。出于这些原因,此规则检测任何始终为真/假的枚举比较,并强制您将其重写为match ($enum)
。
自PHPStan 1.10.34版本以来,其行为要好得多,因为它在最后一个elseif后面跟随else并抛出异常的情况下不会报告错误。这种情况下,如果添加新的枚举情况,您的测试会抛出异常,但在PHPStan中仍然保持沉默。这为错误部署到生产环境留下了空间。因此,我们仍然认为即使在最新的PHPStan中,此规则也是有意义的。
enforceIteratorToArrayPreserveKeys
- 强制在
iterator_to_array
调用中存在第二个参数($preserve_keys
),因为默认值true
通常很危险(数据丢失/失败的风险) - 您可以使用
true
和false
,但现在这样做是故意的选择
$fn = function () { yield new stdClass => 1; }; iterator_to_array($fn()); // denied, would fail
enforceListReturn
- 强制在类方法或函数始终返回列表时使用
list<T>
- 如果方法中只有一个返回空数组的返回,则不将其视为列表
- 当在PHPStan中禁用
list types
时(list types),不执行任何操作 - 考虑在原生的PHPStan中也启用reportAnyTypeWideningInVarTag,因为它主要影响列表
/** * @return array<string> */ public function returnList(): array // error, return phpdoc is generic array, but list is always returned { return ['item']; }
enforceNativeReturnTypehint
- 强制使用受您PHP版本支持的本地返回类型提示
- 如果存在PHPDoc,则从该文档中推断出所需类型提示,如果没有,则根据实际返回类型执行推断
- 适用于类方法、闭包和函数
- 如果您使用
treatPhpDocTypesAsCertain: false
设置了PHPStan,则禁用 - 限制
- 不建议父类型提示
- 忽略trait方法
class NoNativeReturnTypehint { /** * @return list<string> */ public function returnList() // error, missing array typehint { return ['item']; } }
enforceReadonlyPublicProperty
- 通过强制
readonly
修饰符确保所有公共属性的不变性 - 在PHP 8.2中,对于只读类不需要修饰符
- 如果PHP版本不支持只读属性(PHP 8.0及以下),则不执行任何操作
class EnforceReadonlyPublicPropertyRule { public int $foo; // fails, no readonly modifier public readonly int $bar; }
forbidArithmeticOperationOnNonNumber
- 不允许使用非数值类型(仅允许float和int)的算术运算符
- 您可以使用
allowNumericString: true
配置允许数字字符串 - 取模运算符(
%
)仅允许整数,因为它会发出弃用警告 - 加法运算符允许用于合并数组
function add(string $a, string $b) { return $a + $b; // denied, non-numeric types are allowed }
forbidCast
- 拒绝您配置的转换
- 可用的值
(array)
- 默认拒绝(object)
- 默认拒绝(unset)
- 默认拒绝(bool)
(int)
(string)
(float)
- 禁止使用(double)
和(real)
$empty = (array) null; // denied cast $notEmpty = (array) 0; // denied cast
parameters: shipmonkRules: forbidCast: blacklist!: ['(array)', '(object)', '(unset)']
forbidCheckedExceptionInCallable
- 拒绝在可调用对象(闭包、箭头函数和一等可调用对象)中抛出受检查的异常,因为这些异常无法通过PHPStan分析进行跟踪,因为不知道可调用对象何时将被调用
- 允许在立即调用的可调用对象中抛出受检异常(例如,由
@param-immediately-invoked-callable
标记的参数,详见文档) - 允许配置函数/方法,其中可调用对象处理所有抛出的异常,并可以从那里安全地抛出任何内容;这基本上使此类调用被此规则忽略
- 忽略隐式抛出的 Throwable
- 在🇨🇿 关于受检异常的一般性讨论(🇺🇸 幻灯片)中了解更多
parameters: shipmonkRules: forbidCheckedExceptionInCallable: allowedCheckedExceptionCallables: 'Symfony\Component\Console\Question::setValidator': 0 # symfony automatically converts all thrown exceptions to error output, so it is safe to throw anything here
- 我们建议使用以下配置来处理受检异常
- 此外,bleedingEdge可以正确分析多捕获中的死类型,因此我们建议启用此功能
parameters: exceptions: check: missingCheckedExceptionInThrows: true # enforce checked exceptions to be stated in @throws tooWideThrowType: true # report invalid @throws (exceptions that are not actually thrown in annotated method) implicitThrows: false # no @throws means nothing is thrown (otherwise Throwable is thrown) checkedExceptionClasses: - YourApp\TopLevelRuntimeException # track only your exceptions (children of some, typically RuntimeException)
class TransactionManager { /** * @param-immediately-invoked-callable $callback */ public function transactional(callable $callback): void { // ... $callback(); // ... } } class UserEditFacade { /** * @throws UserNotFoundException */ public function updateUserEmail(UserId $userId, Email $email): void { $this->transactionManager->transactional(function () use ($userId, $email) { $user = $this->userRepository->get($userId); // can throw checked UserNotFoundException $user->updateEmail($email); }) } public function getUpdateEmailCallback(UserId $userId, Email $email): callable { return function () use ($userId, $email) { $user = $this->userRepository->get($userId); // this usage is denied, it throws checked exception, but you don't know when, thus it cannot be tracked by phpstan $user->updateEmail($email); }; } }
forbidCheckedExceptionInYieldingMethod
- 拒绝在生成器方法中抛出受检异常,因为这些异常不是在方法调用时抛出,而是在生成器迭代时抛出。
- 此行为无法在PHPStan异常分析中轻松反映,并可能导致错误的否定。
- 确保您已启用受检异常,否则此规则不起作用
class Provider { /** @throws CheckedException */ public static function generate(): iterable { yield 1; throw new CheckedException(); // denied, gets thrown once iterated } }
forbidCustomFunctions *
- 通过拒绝类、方法和函数,您可以轻松拒绝代码库中的一些方法
- 配置语法是数组,其中键是方法名称,值是错误消息中使用的理由
- 即使与接口、构造函数和一些动态类/方法名称(如
$fn = 'sleep'; $fn();
)一起工作
parameters: shipmonkRules: forbidCustomFunctions: list: 'Namespace\SomeClass::*': 'Please use different class' # deny all methods by using * (including constructor) 'Namespace\AnotherClass::someMethod': 'Please use anotherMethod' # deny single method 'var_dump': 'Please remove debug code' # deny function
new SomeClass(); // Class SomeClass is forbidden. Please use different class (new AnotherClass())->someMethod(); // Method AnotherClass::someMethod() is forbidden. Please use anotherMethod
forbidEnumInFunctionArguments
- 保护将本地枚举传递给本地函数,否则会失败/产生警告或产生意外行为
- 大多数数组操作函数不与枚举一起工作,因为它们在内部执行隐式__toString转换,但枚举无法这样做
- 查看测试以了解所有函数及其问题
enum MyEnum: string { case MyCase = 'case1'; } implode('', [MyEnum::MyCase]); // denied, would fail on implicit toString conversion
forbidFetchOnMixed
- 拒绝在未知类型上执行常量/属性获取。
- 任何属性获取都假定调用者是一个具有该属性的对象,因此应修复类型提示/phpdoc。
- 类似于
forbidMethodCallOnMixed
- 仅在PHPStan级别8或以下时才有意义,在级别9上自动禁用
function example($unknown) { $unknown->property; // cannot fetch property on mixed }
forbidIdenticalClassComparison
- 拒绝通过
===
或!==
比较配置的类 - 默认配置仅包含
DateTimeInterface
- 您可能需要添加更多来自您代码库或供应商的类
function isEqual(DateTimeImmutable $a, DateTimeImmutable $b): bool { return $a === $b; // comparing denied classes }
parameters: shipmonkRules: forbidIdenticalClassComparison: blacklist!: - DateTimeInterface - Brick\Money\MoneyContainer - Brick\Math\BigNumber - Ramsey\Uuid\UuidInterface
forbidIncrementDecrementOnNonInteger
- 拒绝使用任何非整数上的
$i++
、$i--
、++$i
、--$i
- PHP本身正在向更严格的行为发展,并在8.3中软弃用一些非整数用法,请参阅RFC
$value = '2e0'; $value++; // would be float(3), denied
forbidMatchDefaultArmForEnums
- 拒绝在将本地枚举作为主题传递给
match()
构造时使用默认臂 - 此规则仅作为本地phpstan规则的补充,该规则保护所有枚举情况都在匹配臂中处理
- 因此,当添加新的枚举情况时,您被迫添加新的臂。这会将您的代码库中所有需要新处理的地方都找出来。
match ($enum) { MyEnum::Case: 1; default: 2; // default arm forbidden }
forbidMethodCallOnMixed
- 拒绝在未知类型上调用方法
- 任何方法调用都假设调用者具有该方法的对象,因此应固定类型提示/PHPDoc。
- 类似于
forbidFetchOnMixed
- 仅在PHPStan级别8或以下时才有意义,在级别9上自动禁用
function example($unknown) { $unknown->call(); // cannot call method on mixed }
forbidNotNormalizedType
- 报告未规范化的PhpDoc或本地类型,这可能发生在
- 当子类和父类出现在其并集或交集时
- 当同一类型在并集或交集中多次出现时
- 当未使用DNF时
- 可通过
checkDisjunctiveNormalForm
配置
- 可通过
- 支持
- 参数类型提示 &
@param
PhpDoc - 返回类型提示 &
@return
PhpDoc - 属性类型提示 &
@var
PhpDoc - 内联
@var
PhpDoc @throws
PhpDoc- 多捕获语句
- 参数类型提示 &
- 主要动机在于PHPStan在分析前对所有类型进行规范化,因此最好以与PHPStan相同的方式在代码库中查看
/** * @return mixed|false // denied, this is still just mixed */ public function getAttribute(string $name) { return $this->attributes[$name] ?? false; }
forbidNullInAssignOperations
- 如果右侧涉及null,则拒绝使用赋值运算符
- 您可以通过配置来指定要忽略哪些运算符,默认情况下仅排除
??=
function getCost(int $cost, ?int $surcharge): int { $cost += $surcharge; // denied, adding possibly-null value return $cost; }
forbidNullInBinaryOperations
- 如果任一侧涉及null,则拒绝使用二元运算符
- 您可以配置要忽略哪些运算符。默认情况下,仅忽略
===, !==, ??
- 当使用最新的phpstan-strict-rules 并启用
allowComparingOnlyComparableTypes
时,建议使用以下自定义设置
parameters: shipmonkRules: forbidNullInBinaryOperations: blacklist!: [ '**', '!=', '==', '+', 'and', 'or', '&&', '||', '%', '-', '/', '*', # checked by phpstan-strict-rules '>', '>=', '<', '<=', '<=>', # checked by AllowComparingOnlyComparableTypesRule '===', '!==', '??' # valid with null involved ]
function getFullName(?string $firstName, string $lastName): string { return $firstName . ' ' . $lastName; // denied, null involved in binary operation }
forbidNullInInterpolatedString
- 不允许在双引号字符串中使用可空表达式
- 这可能需要与
forbidNullInBinaryOperations
中的连接运算符(.
)的设置一致,因此如果您在那里将其列入黑名单,则可能希望禁用此规则
public function output(?string $name) { echo "Hello $name!"; // denied, possibly null value }
forbidPhpDocNullabilityMismatchWithNativeTypehint
- 在使用非可空PhpDoc时,不允许有可空的原生类型提示
- 检查方法上的
@return
和@param
以及属性上的@var
- PHPStan 本身允许在 PhpDoc 中使用原生类型的子类型,但将总体类型解析为这些类型的并集,使此类 PhpDoc 实际上无效
/** * @param string $param */ public function sayHello(?string $param) {} // invalid phpdoc not containing null
forbidProtectedEnumMethod
- 不允许在枚举中使用受保护的方法,因为它们无论如何都不能扩展
- 忽略在特质中声明的方法,因为这些方法可能在常规类中被重用
enum MyEnum { protected function isOpen(): bool {} // protected enum method denied }
forbidReturnValueInYieldingMethod
- 除非标记为返回 Generator 作为值,否则不允许在生成器方法中返回值,因为该值只能通过 Generator::getReturn 访问
- 为防止误用,此规则可以配置为更严格的模式,即使返回类型已声明,也会报告此类返回
class Get { public static function oneTwoThree(): iterable { // marked as iterable, caller cannot access the return value by Generator::getReturn yield 1; yield 2; return 3; } } iterator_to_array(Get::oneTwoThree()); // [1, 2] - see https://3v4l.org/Leu9j
parameters: shipmonkRules: forbidReturnValueInYieldingMethod: reportRegardlessOfReturnType: true # optional stricter mode, defaults to false
forbidUnsafeArrayKey
- 拒绝使用非整型非字符串数组键
- PHP 将
null
、float
和bool
转换为最近的 int/string- 您应该有意识地明确这样做
- 这些类型与默认 PHPStan 行为的主要区别在于,默认 PHPStan 允许将它们用作数组键
- 您可以通过
reportMixed
配置来排除报告mixed
键 - 您可以通过
reportInsideIsset
配置来排除报告isset($array[$invalid])
和$array[$invalid] ?? null
$taxRates = [ // denied, float key gets casted to int (causing $taxRates to contain one item) 1.15 => 'reduced', 1.21 => 'standard', ];
parameters: shipmonkRules: forbidUnsafeArrayKey: reportMixed: false # defaults to true reportInsideIsset: false # defaults to true
forbidVariableTypeOverwriting
- 限制变量赋值到不会改变其类型的情况
- 数组追加
$array[] = 1;
还不支持
- 数组追加
- Null 和 mixed 不考虑在内,在比较之前会裁剪高级 phpstan 类型如 non-empty-X
- 规则允许类型泛化和类型收窄(父类 <-> 子类)
function example(OrderId $id) { $id = $id->getStringValue(); // denied, type changed from object to string }
forbidUnsetClassField
- 拒绝在类字段上调用
unset
,因为这会导致未初始化,请参阅 https://3v4l.org/V8uuP - 应使用 Null 赋值
function example(MyClass $class) { unset($class->field); // denied }
forbidUselessNullableReturn
- 拒绝将返回类型标记为可为空,当从未返回null时
- 建议与
uselessPrivatePropertyDefaultValue
和UselessPrivatePropertyNullabilityRule
一起使用
public function example(int $foo): ?int { // null never returned if ($foo < 0) { return 0; } return $foo; }
forbidUnusedException
- 报告遗忘的异常抛出(由函数创建或返回,但未以任何方式使用)
function validate(): void { new Exception(); // forgotten throw }
forbidUnusedMatchResult
- 报告遗忘的匹配结果使用
- 检查至少有一个返回值的任何
match
match ($foo) { // unused match result case 'foo' => 1; }
requirePreviousExceptionPass
- 检测重新抛出时遗忘的异常传递为上一个异常
- 检查捕获的异常是否可以作为调用(包括构造函数调用)的参数传递到
throw
节点(在catch块内) - 在某些边缘情况下,您可能会遇到假阳性,在这些情况下,您可能不想将异常作为上一个异常传递,请随意忽略这些情况
try { // some code } catch (RuntimeException $e) { throw new LogicException('Cannot happen'); // $e not passed as previous }
- 如果您想要更加严格,可以将
reportEvenIfExceptionIsNotAcceptableByRethrownOne
设置为true
,并且规则将开始报告抛出的异常没有与捕获的异常参数匹配的情况- 默认为true
- 这将迫使您添加参数以便将其作为上一个传递
- 仅在您不从库中抛出异常的情况下可用,而这始终是一种良好的实践
parameters: shipmonkRules: requirePreviousExceptionPass: reportEvenIfExceptionIsNotAcceptableByRethrownOne: true
class MyException extends RuntimeException { public function __construct() { parent::__construct('My error'); } } try { // some code } catch (RuntimeException $e) { throw new MyException(); // reported even though MyException cannot accept it yet }
uselessPrivatePropertyDefaultValue
- 检测始终在构造函数中初始化的私有属性的无用默认值。
- 无法处理构造函数中的条件或私有方法调用。
- 启用时,构造函数中的返回语句被拒绝以避免假阳性
- 建议与
uselessPrivatePropertyNullability
和forbidUselessNullableReturn
一起使用
class Example { private ?int $field = null; // useless default value public function __construct() { $this->field = 1; } }
uselessPrivatePropertyNullability
- 通过检查所有赋值的类型来检测私有属性的无用可空性。
- 仅与原生类型提示的属性一起工作
- 建议与
uselessPrivatePropertyNullability
和forbidUselessNullableReturn
一起使用,因为删除无用的默认值可能会导致检测到无用的可空性 - PHPStan 1.12的 bleeding edge包含此规则的更通用版本,在
property.unusedType
错误标识符下
class Example { private ?int $field; // useless nullability public function __construct() { $this->field = 1; } public function setField(int $value) { $this->field = $value; } }
原生PHPStan额外严格性
PHPStan中的一些严格行为默认未启用,考虑即使在那些情况下启用额外严格性
includes: - phar://phpstan.phar/conf/config.levelmax.neon - phar://phpstan.phar/conf/bleedingEdge.neon # https://phpstan.org/blog/what-is-bleeding-edge - vendor/phpstan/phpstan-strict-rules/rules.neon # https://github.com/phpstan/phpstan-strict-rules parameters: checkImplicitMixed: true # https://phpstan.org/config-reference#checkimplicitmixed checkBenevolentUnionTypes: true # https://phpstan.org/config-reference#checkbenevolentuniontypes checkUninitializedProperties: true # https://phpstan.org/config-reference#checkuninitializedproperties checkMissingCallableSignature: true # https://phpstan.org/config-reference#vague-typehints checkTooWideReturnTypesInProtectedAndPublicMethods: true # https://phpstan.org/config-reference#checktoowidereturntypesinprotectedandpublicmethods reportAnyTypeWideningInVarTag: true # https://phpstan.org/config-reference#reportanytypewideninginvartag reportPossiblyNonexistentConstantArrayOffset: true # https://phpstan.org/config-reference#reportpossiblynonexistentconstantarrayoffset reportPossiblyNonexistentGeneralArrayOffset: true # https://phpstan.org/config-reference#reportpossiblynonexistentgeneralarrayoffset
贡献
- 通过
composer check
检查您的代码 - 通过
composer fix:cs
自动修复编码风格 - 所有功能都必须经过测试