diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 18a7e5c81a..ac4c0366e8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -708,6 +708,12 @@ parameters: count: 2 path: src/Rules/RuleErrorBuilder.php + - + rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Rules/RuleLevelHelper.php + - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 1c1bd926f9..31ef304e3a 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -11,6 +11,7 @@ use PHPStan\Type\CallableType; use PHPStan\Type\ClosureType; use PHPStan\Type\ErrorType; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; @@ -49,6 +50,22 @@ public function __construct( { } + /** + * Skip the mapper on this node and map its children instead. For a generic + * object that avoids applying the nullability relaxation to invariant type + * arguments; GenericObjectType::traverse() rebuilds the same subclass. + * + * @param callable(Type): Type $traverse + */ + private function traverseWithoutMapping(Type $type, callable $traverse): Type + { + if ($type instanceof GenericObjectType) { + return $type->traverse($traverse); + } + + return $traverse($type); + } + /** @api */ public function isThis(Expr $expression): bool { @@ -95,7 +112,7 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): return new CallableType( $acceptedType->getParameters(), - $traverse($acceptedType->getReturnType()), + $this->traverseWithoutMapping($acceptedType->getReturnType(), $traverse), $acceptedType->isVariadic(), $acceptedType->getTemplateTypeMap(), $acceptedType->getResolvedTemplateTypeMap(), @@ -111,7 +128,7 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): return new ClosureType( $acceptedType->getParameters(), - $traverse($acceptedType->getReturnType()), + $this->traverseWithoutMapping($acceptedType->getReturnType(), $traverse), $acceptedType->isVariadic(), $acceptedType->getTemplateTypeMap(), $acceptedType->getResolvedTemplateTypeMap(), @@ -127,6 +144,10 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): ); } + if ($acceptedType instanceof GenericObjectType) { + return $acceptedType->traverse($traverse); + } + if ( !$this->checkNullables && !$acceptingType instanceof NullType diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index e6d9639a74..d3fc29d8ba 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3023,6 +3023,21 @@ public function testCallablesWithoutCheckNullables(bool $checkNullables, bool $c $this->analyse([__DIR__ . '/data/callables-without-check-nullables.php'], $expectedErrors); } + public static function dataGenericArgumentNullability(): iterable + { + yield [false]; + yield [true]; + } + + #[DataProvider('dataGenericArgumentNullability')] + public function testGenericArgumentNullability(bool $checkNullables): void + { + $this->checkThisOnly = false; + $this->checkNullables = $checkNullables; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/generic-argument-nullability.php'], []); + } + #[RequiresPhp('>= 8.0.0')] public function testBug8713(): void { diff --git a/tests/PHPStan/Rules/Methods/data/generic-argument-nullability.php b/tests/PHPStan/Rules/Methods/data/generic-argument-nullability.php new file mode 100644 index 0000000000..4d8dc51cd7 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/generic-argument-nullability.php @@ -0,0 +1,57 @@ +): T $cb + * @return T + */ + public function grabTwoArgs(callable $cb) + { + return $cb(null, []); + } + + /** @param Collection $collection */ + public function acceptNullable(Collection $collection): void + { + } + +} + +/** + * @param Collection $plain + * @param Collection $nullable + * @param array $array + */ +function test(Foo $foo, Collection $plain, Collection $nullable, array $array, ?int $scalar): void +{ + $foo->grab(fn () => $plain); + $foo->grab(fn () => $nullable); + $foo->grab(fn () => $array); + $foo->grab(fn () => $scalar); + $foo->grabTwoArgs(fn () => $nullable); + $foo->acceptNullable($nullable); +}