shipmonk/phpstan-rules

我们在ShipMonk项目中找到的各种额外严格的PHPStan规则。

安装次数: 417,244

依赖者: 36

建议者: 0

安全: 0

星星: 102

关注者: 5

分支: 11

开放性问题: 2

类型:phpstan-extension


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通常很危险(数据丢失/失败的风险)
  • 您可以使用truefalse,但现在这样做是故意的选择
$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 将 nullfloatbool 转换为最近的 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时
  • 建议与uselessPrivatePropertyDefaultValueUselessPrivatePropertyNullabilityRule一起使用
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

  • 检测始终在构造函数中初始化的私有属性的无用默认值。
  • 无法处理构造函数中的条件或私有方法调用。
  • 启用时,构造函数中的返回语句被拒绝以避免假阳性
  • 建议与uselessPrivatePropertyNullabilityforbidUselessNullableReturn一起使用
class Example
{
    private ?int $field = null; // useless default value

    public function __construct()
    {
        $this->field = 1;
    }
}

uselessPrivatePropertyNullability

  • 通过检查所有赋值的类型来检测私有属性的无用可空性。
  • 仅与原生类型提示的属性一起工作
  • 建议与uselessPrivatePropertyNullabilityforbidUselessNullableReturn一起使用,因为删除无用的默认值可能会导致检测到无用的可空性
  • 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自动修复编码风格
  • 所有功能都必须经过测试