From ba758904c796f65484d5f16f6ddc63ae5b992f8d Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Mon, 24 Oct 2022 16:34:02 +0200 Subject: [PATCH 1/2] Accept and describe callable-object/callable-string --- src/PhpDoc/TypeNodeResolver.php | 3 ++ src/Type/IntersectionType.php | 27 +++++++++++++---- .../Analyser/NodeScopeResolverTest.php | 3 ++ .../PHPStan/Analyser/data/callable-object.php | 29 +++++++++++++++++++ .../PHPStan/Analyser/data/callable-string.php | 28 ++++++++++++++++++ tests/PHPStan/Analyser/data/param-out.php | 2 +- tests/PHPStan/Type/TypeCombinatorTest.php | 4 +-- 7 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/callable-object.php create mode 100644 tests/PHPStan/Analyser/data/callable-string.php diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index d4f9fff964..84862b88a7 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -325,6 +325,9 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'object': return new ObjectWithoutClassType(); + case 'callable-object': + return new IntersectionType([new ObjectWithoutClassType(), new CallableType()]); + case 'never': $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 791a5129cc..49760c901c 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -200,6 +200,7 @@ function () use ($level): string { private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes): string { + $baseTypes = []; $typesToDescribe = []; $skipTypeNames = []; @@ -238,11 +239,19 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) continue; } - if ($skipAccessoryTypes) { + if ($type instanceof CallableType && $type->isCommonCallable()) { + $typesToDescribe[] = $type; + $skipTypeNames[] = 'object'; + $skipTypeNames[] = 'string'; continue; } if (!$type instanceof AccessoryType) { + $baseTypes[] = $type; + continue; + } + + if ($skipAccessoryTypes) { continue; } @@ -250,11 +259,19 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) } $describedTypes = []; - foreach ($this->getSortedTypes() as $type) { - if ($type instanceof AccessoryType) { - continue; - } + foreach ($baseTypes as $type) { $typeDescription = $type->describe($level); + + if (in_array($typeDescription, ['object', 'string'], true) && in_array($typeDescription, $skipTypeNames, true)) { + foreach ($typesToDescribe as $j => $typeToDescribe) { + if ($typeToDescribe instanceof CallableType && $typeToDescribe->isCommonCallable()) { + $describedTypes[] = 'callable-' . $typeDescription; + unset($typesToDescribe[$j]); + continue 2; + } + } + } + if ( substr($typeDescription, 0, strlen('array<')) === 'array<' && in_array('array', $skipTypeNames, true) diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index ef03074841..e1654ce862 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1095,6 +1095,9 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7519.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8087.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5785.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/callable-object.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/callable-string.php'); } /** diff --git a/tests/PHPStan/Analyser/data/callable-object.php b/tests/PHPStan/Analyser/data/callable-object.php new file mode 100644 index 0000000000..c262c30b34 --- /dev/null +++ b/tests/PHPStan/Analyser/data/callable-object.php @@ -0,0 +1,29 @@ + Date: Tue, 25 Oct 2022 09:19:10 +0200 Subject: [PATCH 2/2] Retain sort order in intersection type --- src/Type/IntersectionType.php | 27 ++++++++++--------- .../PHPStan/Analyser/data/callable-object.php | 9 +++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 49760c901c..bd5660cd48 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -30,6 +30,7 @@ use function count; use function implode; use function in_array; +use function ksort; use function sprintf; use function strlen; use function substr; @@ -206,7 +207,7 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) $nonEmptyStr = false; $nonFalsyStr = false; - foreach ($this->getSortedTypes() as $type) { + foreach ($this->getSortedTypes() as $i => $type) { if ($type instanceof AccessoryNonEmptyStringType || $type instanceof AccessoryLiteralStringType || $type instanceof AccessoryNumericStringType @@ -229,25 +230,25 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) } } - $typesToDescribe[] = $type; + $typesToDescribe[$i] = $type; $skipTypeNames[] = 'string'; continue; } if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) { - $typesToDescribe[] = $type; + $typesToDescribe[$i] = $type; $skipTypeNames[] = 'array'; continue; } if ($type instanceof CallableType && $type->isCommonCallable()) { - $typesToDescribe[] = $type; + $typesToDescribe[$i] = $type; $skipTypeNames[] = 'object'; $skipTypeNames[] = 'string'; continue; } if (!$type instanceof AccessoryType) { - $baseTypes[] = $type; + $baseTypes[$i] = $type; continue; } @@ -255,17 +256,17 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) continue; } - $typesToDescribe[] = $type; + $typesToDescribe[$i] = $type; } $describedTypes = []; - foreach ($baseTypes as $type) { + foreach ($baseTypes as $i => $type) { $typeDescription = $type->describe($level); if (in_array($typeDescription, ['object', 'string'], true) && in_array($typeDescription, $skipTypeNames, true)) { foreach ($typesToDescribe as $j => $typeToDescribe) { if ($typeToDescribe instanceof CallableType && $typeToDescribe->isCommonCallable()) { - $describedTypes[] = 'callable-' . $typeDescription; + $describedTypes[$i] = 'callable-' . $typeDescription; unset($typesToDescribe[$j]); continue 2; } @@ -298,7 +299,7 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) $typeName = 'non-empty-' . $typeName; } - $describedTypes[] = $typeName . '<' . substr($typeDescription, strlen('array<')); + $describedTypes[$i] = $typeName . '<' . substr($typeDescription, strlen('array<')); continue; } @@ -306,13 +307,15 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) continue; } - $describedTypes[] = $type->describe($level); + $describedTypes[$i] = $type->describe($level); } - foreach ($typesToDescribe as $typeToDescribe) { - $describedTypes[] = $typeToDescribe->describe($level); + foreach ($typesToDescribe as $i => $typeToDescribe) { + $describedTypes[$i] = $typeToDescribe->describe($level); } + ksort($describedTypes); + return implode('&', $describedTypes); } diff --git a/tests/PHPStan/Analyser/data/callable-object.php b/tests/PHPStan/Analyser/data/callable-object.php index c262c30b34..f9f3dee69c 100644 --- a/tests/PHPStan/Analyser/data/callable-object.php +++ b/tests/PHPStan/Analyser/data/callable-object.php @@ -2,6 +2,7 @@ namespace CallableObject; +use Iterator; use function PHPStan\Testing\assertType; /** @@ -27,3 +28,11 @@ function foo(callable $callable, $object, $callableObject) assertType('callable-object', $object); } } + +/** + * @param Iterator&callable $object + */ +function bar($object) +{ + assertType('callable(): mixed&Iterator', $object); +}