From 16685d2e8c24e06c02751ab7bf2b478c9e78c3c1 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 6 Aug 2025 11:47:35 +0200 Subject: [PATCH] POC --- src/Rules/RuleLevelHelper.php | 26 ++++++++---- .../Functions/ClosureReturnTypeRuleTest.php | 11 ++++- .../Rules/Functions/data/bug-12008.php | 42 +++++++++++++++++++ 3 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-12008.php diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 109b30a2b3..e7e7366007 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\CallableType; @@ -16,6 +17,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; +use PHPStan\Type\SimultaneousTypeTraverser; use PHPStan\Type\StrictMixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -87,15 +89,18 @@ private function transformCommonType(Type $type): Type private function transformAcceptedType(Type $acceptingType, Type $acceptedType): array { $checkForUnion = $this->checkUnionTypes; - $acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type { + $acceptedType = SimultaneousTypeTraverser::map($acceptedType, $acceptingType, function (Type $acceptedType, Type $acceptingType, callable $traverse) use (&$checkForUnion): Type { if ($acceptedType instanceof CallableType) { if ($acceptedType->isCommonCallable()) { return $acceptedType; } + if (!$acceptingType instanceof ParametersAcceptor) { + return $acceptedType; + } return new CallableType( $acceptedType->getParameters(), - $traverse($this->transformCommonType($acceptedType->getReturnType())), + $traverse($this->transformCommonType($acceptedType->getReturnType()), $acceptingType->getReturnType()), $acceptedType->isVariadic(), $acceptedType->getTemplateTypeMap(), $acceptedType->getResolvedTemplateTypeMap(), @@ -109,9 +114,13 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): return $acceptedType; } + if (!$acceptingType instanceof ParametersAcceptor) { + return $acceptedType; + } + return new ClosureType( $acceptedType->getParameters(), - $traverse($this->transformCommonType($acceptedType->getReturnType())), + $traverse($this->transformCommonType($acceptedType->getReturnType()), $acceptingType->getReturnType()), $acceptedType->isVariadic(), $acceptedType->getTemplateTypeMap(), $acceptedType->getResolvedTemplateTypeMap(), @@ -127,21 +136,22 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): if ( !$this->checkNullables - && !$acceptingType instanceof NullType - && !$acceptedType instanceof NullType && !$acceptedType instanceof BenevolentUnionType + && !$acceptedType instanceof NullType + && TypeCombinator::containsNull($acceptedType) + && !TypeCombinator::containsNull($acceptingType) ) { - return $traverse(TypeCombinator::removeNull($acceptedType)); + return $traverse(TypeCombinator::removeNull($acceptedType), $acceptingType); } if ($this->checkBenevolentUnionTypes) { if ($acceptedType instanceof BenevolentUnionType) { $checkForUnion = true; - return $traverse(TypeUtils::toStrictUnion($acceptedType)); + return $traverse(TypeUtils::toStrictUnion($acceptedType), $acceptingType); } } - return $traverse($this->transformCommonType($acceptedType)); + return $traverse($this->transformCommonType($acceptedType), $acceptingType); }); return [$acceptedType, $checkForUnion]; diff --git a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php index 38d4ec65d8..350efa7848 100644 --- a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php @@ -13,9 +13,11 @@ class ClosureReturnTypeRuleTest extends RuleTestCase { + private bool $checkNullables = true; + protected function getRule(): Rule { - return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true))); + return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper(self::createReflectionProvider(), $this->checkNullables, false, true, false, false, false, true))); } public function testClosureReturnTypeRule(): void @@ -128,6 +130,13 @@ public function testBug7220(): void $this->analyse([__DIR__ . '/data/bug-7220.php'], []); } + public function testBug12008(): void + { + $this->checkNullables = false; + + $this->analyse([__DIR__ . '/data/bug-12008.php'], []); + } + public function testBugFunctionMethodConstants(): void { $this->analyse([__DIR__ . '/data/bug-anonymous-function-method-constant.php'], []); diff --git a/tests/PHPStan/Rules/Functions/data/bug-12008.php b/tests/PHPStan/Rules/Functions/data/bug-12008.php new file mode 100644 index 0000000000..47e3ef08c7 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12008.php @@ -0,0 +1,42 @@ + $records + */ + public function __construct( + public iterable $records, + ) { + } +} + +class HelloWorld +{ + private function respondToApiRequest(Closure|null $data): never { + exit; + } + + /** + * @param list $products + */ + public function run(array $products): never { + $this->respondToApiRequest(function () use ($products) { + return new Pagination(array_map( + fn (ProductOverview $product) => [ + 'id' => $product->getId(), + ], + $products, + )); + }); + } +}