From ffa768688859913fde122d96c98cdabe49b04105 Mon Sep 17 00:00:00 2001 From: Brad Miller <28307684+mad-briller@users.noreply.github.com> Date: Tue, 20 Feb 2024 18:18:44 +0000 Subject: [PATCH] Fix how union of callables is understood --- .../ArrayMapFunctionReturnTypeExtension.php | 5 +- src/Type/UnionType.php | 11 ++- .../Analyser/NodeScopeResolverTest.php | 3 + tests/PHPStan/Analyser/data/bug-10283.php | 25 +++++++ tests/PHPStan/Analyser/data/bug-10442.php | 15 ++++ tests/PHPStan/Analyser/data/bug-6633.php | 75 +++++++++++++++++++ .../Rules/Functions/CallCallablesRuleTest.php | 20 +++++ .../Rules/Functions/data/bug-3818b.php | 29 +++++++ .../PHPStan/Rules/Functions/data/bug-6633.php | 71 ++++++++++++++++++ .../PHPStan/Rules/Functions/data/bug-9594.php | 26 +++++++ .../PHPStan/Rules/Functions/data/bug-9614.php | 27 +++++++ 11 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-10283.php create mode 100644 tests/PHPStan/Analyser/data/bug-10442.php create mode 100644 tests/PHPStan/Analyser/data/bug-6633.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-3818b.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-6633.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-9594.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-9614.php diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index 0e17d33c35..50841093a8 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -39,10 +39,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $callableIsNull = $callableType->isNull()->yes(); if ($callableType->isCallable()->yes()) { - $valueType = new NeverType(); + $valueTypes = [new NeverType()]; foreach ($callableType->getCallableParametersAcceptors($scope) as $parametersAcceptor) { - $valueType = TypeCombinator::union($valueType, $parametersAcceptor->getReturnType()); + $valueTypes[] = $parametersAcceptor->getReturnType(); } + $valueType = TypeCombinator::union(...$valueTypes); } elseif ($callableIsNull) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach (array_slice($functionCall->getArgs(), 1) as $index => $arg) { diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 90cbf81dc4..81fb230132 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -28,6 +28,7 @@ use PHPStan\Type\Generic\TemplateUnionType; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use function array_map; +use function array_merge; use function array_unique; use function array_values; use function count; @@ -708,15 +709,21 @@ public function isCallable(): TrinaryLogic */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { + $acceptors = []; + foreach ($this->types as $type) { if ($type->isCallable()->no()) { continue; } - return $type->getCallableParametersAcceptors($scope); + $acceptors = array_merge($acceptors, $type->getCallableParametersAcceptors($scope)); + } + + if (count($acceptors) === 0) { + throw new ShouldNotHappenException(); } - throw new ShouldNotHappenException(); + return $acceptors; } public function isCloneable(): TrinaryLogic diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 0a9424b198..17346a6e5b 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -61,6 +61,9 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/strtotime-return-type-extensions.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type-extensions.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-key.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6633.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10283.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10442.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-9972.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/intersection-static.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/static-properties.php'); diff --git a/tests/PHPStan/Analyser/data/bug-10283.php b/tests/PHPStan/Analyser/data/bug-10283.php new file mode 100644 index 0000000000..e2cb63e31e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10283.php @@ -0,0 +1,25 @@ +name . $this->version; + } +} + +class ServiceRedis +{ + public function __construct( + private string $name, + private string $version, + private bool $persistent, + ) {} + + public function touchAll() : string{ + return $this->persistent ? $this->name : $this->version; + } +} + +function test(?string $type = NULL) : void { + $types = [ + 'solr' => [ + 'label' => 'SOLR Search', + 'data_class' => CreateServiceSolrData::class, + 'to_entity' => function (CreateServiceSolrData $data) { + assert($data->name !== NULL && $data->version !== NULL, "Incorrect form validation"); + return new ServiceSolr($data->name, $data->version); + }, + ], + 'redis' => [ + 'label' => 'Redis', + 'data_class' => CreateServiceRedisData::class, + 'to_entity' => function (CreateServiceRedisData $data) { + assert($data->name !== NULL && $data->version !== NULL && $data->persistent !== NULL, "Incorrect form validation"); + return new ServiceRedis($data->name, $data->version, $data->persistent); + }, + ], + ]; + + if ($type === NULL || !isset($types[$type])) { + throw new \RuntimeException("404 or choice form here"); + } + + $data = new $types[$type]['data_class'](); + + $service = $types[$type]['to_entity']($data); + + assertType('Bug6633\ServiceRedis|Bug6633\ServiceSolr', $service); +} diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index f069801781..bc20e208da 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -286,4 +286,24 @@ public function testBug6485(): void ]); } + public function testBug6633(): void + { + $this->analyse([__DIR__ . '/data/bug-6633.php'], []); + } + + public function testBug3818b(): void + { + $this->analyse([__DIR__ . '/data/bug-3818b.php'], []); + } + + public function testBug9594(): void + { + $this->analyse([__DIR__ . '/data/bug-9594.php'], []); + } + + public function testBug9614(): void + { + $this->analyse([__DIR__ . '/data/bug-9614.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-3818b.php b/tests/PHPStan/Rules/Functions/data/bug-3818b.php new file mode 100644 index 0000000000..0a9a3d6c83 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3818b.php @@ -0,0 +1,29 @@ +handleA(...) : $this->handleB(...); + + $method($obj); + } + + private function handleA(A $a): void + { + } + + private function handleB(B $b): void + { + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-6633.php b/tests/PHPStan/Rules/Functions/data/bug-6633.php new file mode 100644 index 0000000000..31bc3cf7d8 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6633.php @@ -0,0 +1,71 @@ +name . $this->version; + } +} + +class ServiceRedis +{ + public function __construct( + private string $name, + private string $version, + private bool $persistent, + ) {} + + public function touchAll() : string{ + return $this->persistent ? $this->name : $this->version; + } +} + +function test(?string $type = NULL) : void { + $types = [ + 'solr' => [ + 'label' => 'SOLR Search', + 'data_class' => CreateServiceSolrData::class, + 'to_entity' => function (CreateServiceSolrData $data) { + assert($data->name !== NULL && $data->version !== NULL, "Incorrect form validation"); + return new ServiceSolr($data->name, $data->version); + }, + ], + 'redis' => [ + 'label' => 'Redis', + 'data_class' => CreateServiceRedisData::class, + 'to_entity' => function (CreateServiceRedisData $data) { + assert($data->name !== NULL && $data->version !== NULL && $data->persistent !== NULL, "Incorrect form validation"); + return new ServiceRedis($data->name, $data->version, $data->persistent); + }, + ], + ]; + + if ($type === NULL || !isset($types[$type])) { + throw new \RuntimeException("404 or choice form here"); + } + + $data = new $types[$type]['data_class'](); + + $service = $types[$type]['to_entity']($data); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9594.php b/tests/PHPStan/Rules/Functions/data/bug-9594.php new file mode 100644 index 0000000000..9e5a0b96f7 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9594.php @@ -0,0 +1,26 @@ + [1, 2, 3], + 'greet' => fn (int $value) => 'I am '.$value, + ], + [ + 'elements' => ['hello', 'world'], + 'greet' => fn (string $value) => 'I am '.$value, + ], + ]; + + foreach ($data as $entry) { + foreach ($entry['elements'] as $element) { + $entry['greet']($element); + } + } + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9614.php b/tests/PHPStan/Rules/Functions/data/bug-9614.php new file mode 100644 index 0000000000..1209501f2e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9614.php @@ -0,0 +1,27 @@ + function() { + return 'test'; + }, + 'foo' => function($a) { + return 'foo'; + }, + 'bar' => function($a, $b) { + return 'bar'; + } + ]; + + if (!isset($funcs[$key])) { + return ''; + } + + return $funcs[$key]($a, $b); + } +}