diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 4df978002b..d4145f27bb 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -1051,6 +1051,10 @@ private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $name } foreach ($typeNode->items as $itemNode) { + if ($itemNode->valueType instanceof CallableTypeNode) { + $builder->disableClosureDegradation(); + } + $offsetType = $this->resolveArrayShapeOffsetType($itemNode, $nameScope); $builder->setOffsetValueType($offsetType, $this->resolve($itemNode->valueType, $nameScope), $itemNode->optional); } diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index 9396fec39f..4d919a0e5a 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -31,11 +31,11 @@ final class ConstantArrayTypeBuilder { public const ARRAY_COUNT_LIMIT = 256; - private const CLOSURES_COUNT_LIMIT = 16; + private const CLOSURES_COUNT_LIMIT = 32; private bool $degradeToGeneralArray = false; - private bool $degradeClosures = false; + private ?bool $degradeClosures = null; private bool $oversized = false; @@ -84,7 +84,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt } if (!$this->degradeToGeneralArray) { - if ($valueType instanceof ClosureType) { + if ($valueType instanceof ClosureType && $this->degradeClosures !== false) { $numClosures = 1; foreach ($this->valueTypes as $innerType) { if (!($innerType instanceof ClosureType)) { @@ -300,6 +300,11 @@ public function degradeToGeneralArray(bool $oversized = false): void $this->oversized = $this->oversized || $oversized; } + public function disableClosureDegradation(): void + { + $this->degradeClosures = false; + } + public function getArray(): Type { $keyTypesCount = count($this->keyTypes); @@ -313,7 +318,7 @@ public function getArray(): Type return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } - if ($this->degradeClosures) { + if ($this->degradeClosures === true) { $itemTypes = []; $itemTypes[] = new CallableType(); foreach ($this->valueTypes as $valueType) { diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 92c6565789..9804bccd47 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -247,6 +247,7 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Methods/data/bug-4801.php'; yield __DIR__ . '/../Rules/Arrays/data/narrow-superglobal.php'; yield __DIR__ . '/../Rules/Methods/data/bug-12927.php'; + yield __DIR__ . '/../Rules/Properties/data/bug-14012.php'; } /** diff --git a/tests/PHPStan/Analyser/nsrt/degrade-closures.php b/tests/PHPStan/Analyser/nsrt/degrade-closures.php index a310d0d5f3..a7be72480e 100644 --- a/tests/PHPStan/Analyser/nsrt/degrade-closures.php +++ b/tests/PHPStan/Analyser/nsrt/degrade-closures.php @@ -20,7 +20,23 @@ $arr[] = static function () {}; $arr[] = static function () {}; $arr[] = static function () {}; -assertType('array{Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void}', $arr); +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +$arr[] = static function () {}; +assertType('array{Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void}', $arr); $arr[] = static function () {}; assertType('non-empty-list&oversized-array', $arr); diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index bdd0d7c73a..8b46bbecc9 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2591,4 +2591,11 @@ public function testBug8936(): void $this->analyse([__DIR__ . '/data/bug-8936.php'], []); } + public function testBug14012(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = false; + $this->analyse([__DIR__ . '/../Properties/data/bug-14012.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php index c274aff53c..0da13c8cda 100644 --- a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php @@ -76,4 +76,9 @@ public function testPromotedProperties(): void $this->analyse([__DIR__ . '/data/promoted-properties-missing-typehint.php'], []); } + public function testBug14012(): void + { + $this->analyse([__DIR__ . '/data/bug-14012.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-14012.php b/tests/PHPStan/Rules/Properties/data/bug-14012.php new file mode 100644 index 0000000000..b341fe720a --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-14012.php @@ -0,0 +1,55 @@ +