From 9b7852b3f9aa808cc1a0e0bae02fa28be0788533 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 29 Dec 2025 17:18:17 +0100 Subject: [PATCH 01/10] Speedup analysis of big arrays containing closures degrade analysis precision to a callable type --- src/Type/Constant/ConstantArrayType.php | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index fbb1217057..f792996ef4 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -29,6 +29,8 @@ use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; +use PHPStan\Type\CallableType; +use PHPStan\Type\ClosureType; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ErrorType; @@ -167,7 +169,25 @@ public function getIterableValueType(): Type return $this->iterableValueType; } - return $this->iterableValueType = count($this->valueTypes) > 0 ? TypeCombinator::union(...$this->valueTypes) : new NeverType(true); + $count = count($this->valueTypes); + if ($count === 0) { + return new NeverType(true); + } + + if ($count > 16) { + $onlyClosureValues = true; + foreach ($this->valueTypes as $valueType) { + if (!$valueType instanceof ClosureType) { + $onlyClosureValues = false; + } + } + + if ($onlyClosureValues) { + return new CallableType(); + } + } + + return TypeCombinator::union(...$this->valueTypes); } public function getKeyType(): Type From acd68ef91bc3449d9315903a85e672334b1fceed Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 29 Dec 2025 17:19:31 +0100 Subject: [PATCH 02/10] Update ConstantArrayType.php --- src/Type/Constant/ConstantArrayType.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index f792996ef4..544e4c48fa 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -179,6 +179,7 @@ public function getIterableValueType(): Type foreach ($this->valueTypes as $valueType) { if (!$valueType instanceof ClosureType) { $onlyClosureValues = false; + break; } } From d30007636d66c67ee673d543eb633099cab8cf8e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 29 Dec 2025 17:42:42 +0100 Subject: [PATCH 03/10] added regression test --- .../Analyser/AnalyserIntegrationTest.php | 7 + tests/PHPStan/Analyser/data/bug-13933.php | 246 ++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 tests/PHPStan/Analyser/data/bug-13933.php diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index a3e9095a89..3a46c92ca8 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1570,6 +1570,13 @@ public function testBug13714(): void $this->assertSame('Function Bug13714\array_find invoked with 2 parameters, 0 required.', $errors[6]->getMessage()); } + public function testBug13933(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-13933.php'); + $this->assertNoErrors($errors); + } + + /** * @param string[]|null $allAnalysedFiles * @return list diff --git a/tests/PHPStan/Analyser/data/bug-13933.php b/tests/PHPStan/Analyser/data/bug-13933.php new file mode 100644 index 0000000000..023c61bad3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-13933.php @@ -0,0 +1,246 @@ + Date: Mon, 29 Dec 2025 17:44:25 +0100 Subject: [PATCH 04/10] Update AnalyserIntegrationTest.php --- tests/PHPStan/Analyser/AnalyserIntegrationTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 3a46c92ca8..fed5cb2c75 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1576,7 +1576,6 @@ public function testBug13933(): void $this->assertNoErrors($errors); } - /** * @param string[]|null $allAnalysedFiles * @return list From fb8073a07903c2a61eb417fb48946450888c406c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 29 Dec 2025 17:48:47 +0100 Subject: [PATCH 05/10] Update ConstantArrayType.php --- src/Type/Constant/ConstantArrayType.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 544e4c48fa..028698e42b 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -171,7 +171,7 @@ public function getIterableValueType(): Type $count = count($this->valueTypes); if ($count === 0) { - return new NeverType(true); + return $this->iterableValueType = new NeverType(true); } if ($count > 16) { @@ -184,11 +184,11 @@ public function getIterableValueType(): Type } if ($onlyClosureValues) { - return new CallableType(); + return $this->iterableValueType = new CallableType(); } } - return TypeCombinator::union(...$this->valueTypes); + return $this->iterableValueType = TypeCombinator::union(...$this->valueTypes); } public function getKeyType(): Type From fb2268177e397ec06eda6cf8712be077925647bb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 31 Dec 2025 10:58:52 +0100 Subject: [PATCH 06/10] in ConstantArrayTypeBuilder --- src/Type/Constant/ConstantArrayType.php | 23 +----------- .../Constant/ConstantArrayTypeBuilder.php | 35 ++++++++++++++++++- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 028698e42b..fbb1217057 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -29,8 +29,6 @@ use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; -use PHPStan\Type\CallableType; -use PHPStan\Type\ClosureType; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ErrorType; @@ -169,26 +167,7 @@ public function getIterableValueType(): Type return $this->iterableValueType; } - $count = count($this->valueTypes); - if ($count === 0) { - return $this->iterableValueType = new NeverType(true); - } - - if ($count > 16) { - $onlyClosureValues = true; - foreach ($this->valueTypes as $valueType) { - if (!$valueType instanceof ClosureType) { - $onlyClosureValues = false; - break; - } - } - - if ($onlyClosureValues) { - return $this->iterableValueType = new CallableType(); - } - } - - return $this->iterableValueType = TypeCombinator::union(...$this->valueTypes); + return $this->iterableValueType = count($this->valueTypes) > 0 ? TypeCombinator::union(...$this->valueTypes) : new NeverType(true); } public function getKeyType(): Type diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index b1a68ac083..28df23f4ea 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -8,6 +8,8 @@ use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\ArrayType; +use PHPStan\Type\CallableType; +use PHPStan\Type\ClosureType; use PHPStan\Type\IntersectionType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -29,9 +31,12 @@ final class ConstantArrayTypeBuilder { public const ARRAY_COUNT_LIMIT = 256; + private const ARRAY_CLOSURES_COUNT_LIMIT = 16; private bool $degradeToGeneralArray = false; + private bool $degradeClosures = false; + private bool $oversized = false; /** @@ -79,6 +84,23 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt } if (!$this->degradeToGeneralArray) { + if ($valueType instanceof ClosureType) { + $numClosures = 1; + foreach ($this->valueTypes as $innerType) { + if (!($innerType instanceof ClosureType)) { + continue; + } + + $numClosures++; + } + + if ($numClosures >= self::ARRAY_CLOSURES_COUNT_LIMIT) { + $this->degradeClosures = true; + $this->degradeToGeneralArray = true; + $this->oversized = true; + } + } + if ($offsetType === null) { $newAutoIndexes = $optional ? $this->nextAutoIndexes : []; $hasOptional = false; @@ -291,9 +313,20 @@ public function getArray(): Type return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } + $itemTypes = []; + foreach ($this->valueTypes as $valueType) { + if ($this->degradeClosures && $valueType instanceof ClosureType) { + continue; + } + $itemTypes[] = $valueType; + } + if ($this->degradeClosures) { + $itemTypes[] = new CallableType(); + } + $array = new ArrayType( TypeCombinator::union(...$this->keyTypes), - TypeCombinator::union(...$this->valueTypes), + TypeCombinator::union(...$itemTypes), ); $types = []; From 709f47f48f2a76c0179a777f28633a1b88dbdda7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 31 Dec 2025 11:20:09 +0100 Subject: [PATCH 07/10] Update bug-13933.php --- tests/PHPStan/Analyser/data/bug-13933.php | 65 +++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/PHPStan/Analyser/data/bug-13933.php b/tests/PHPStan/Analyser/data/bug-13933.php index 023c61bad3..711ce8cbee 100644 --- a/tests/PHPStan/Analyser/data/bug-13933.php +++ b/tests/PHPStan/Analyser/data/bug-13933.php @@ -10,6 +10,7 @@ $list['a-4'] = static function (): D { return new D(); }; $list['a-5'] = static function (): E { return new E(); }; $list['a-6'] = static function (): F { return new F(); }; +$list['string'] = 'hello'; // Beta $list['b-1'] = static function (): A1 { return new A1(); }; $list['b-2'] = static function (): B1 { return new B1(); }; @@ -17,6 +18,7 @@ $list['b-4'] = static function (): D1 { return new D1(); }; $list['b-5'] = static function (): E1 { return new E1(); }; $list['b-6'] = static function (): F1 { return new F1(); }; +$list['int'] = 123; // Delta $list['c-1'] = static function (): A2 { return new A2(); }; $list['c-2'] = static function (): B2 { return new B2(); }; @@ -122,6 +124,69 @@ $list['q-4'] = static function (): D16 { return new D16(); }; $list['q-5'] = static function (): E16 { return new E16(); }; $list['q-6'] = static function (): F16 { return new F16(); }; +// +$list['r-1'] = static function (): A16 { return new A16(); }; +$list['r-2'] = static function (): B16 { return new B16(); }; +$list['r-3'] = static function (): C16 { return new C16(); }; +$list['r-4'] = static function (): D16 { return new D16(); }; +$list['r-5'] = static function (): E16 { return new E16(); }; +$list['r-6'] = static function (): F16 { return new F16(); }; +// +$list['s-1'] = static function (): A16 { return new A16(); }; +$list['s-2'] = static function (): B16 { return new B16(); }; +$list['s-3'] = static function (): C16 { return new C16(); }; +$list['s-4'] = static function (): D16 { return new D16(); }; +$list['s-5'] = static function (): E16 { return new E16(); }; +$list['s-6'] = static function (): F16 { return new F16(); }; +// +$list['t-1'] = static function (): A16 { return new A16(); }; +$list['t-2'] = static function (): B16 { return new B16(); }; +$list['t-3'] = static function (): C16 { return new C16(); }; +$list['t-4'] = static function (): D16 { return new D16(); }; +$list['t-5'] = static function (): E16 { return new E16(); }; +$list['t-6'] = static function (): F16 { return new F16(); }; +// +$list['u-1'] = static function (): A16 { return new A16(); }; +$list['u-2'] = static function (): B16 { return new B16(); }; +$list['u-3'] = static function (): C16 { return new C16(); }; +$list['u-4'] = static function (): D16 { return new D16(); }; +$list['u-5'] = static function (): E16 { return new E16(); }; +$list['u-6'] = static function (): F16 { return new F16(); }; +// +$list['v-1'] = static function (): A16 { return new A16(); }; +$list['v-2'] = static function (): B16 { return new B16(); }; +$list['v-3'] = static function (): C16 { return new C16(); }; +$list['v-4'] = static function (): D16 { return new D16(); }; +$list['v-5'] = static function (): E16 { return new E16(); }; +$list['v-6'] = static function (): F16 { return new F16(); }; +// +$list['w-1'] = static function (): A16 { return new A16(); }; +$list['w-2'] = static function (): B16 { return new B16(); }; +$list['w-3'] = static function (): C16 { return new C16(); }; +$list['w-4'] = static function (): D16 { return new D16(); }; +$list['w-5'] = static function (): E16 { return new E16(); }; +$list['w-6'] = static function (): F16 { return new F16(); }; +// +$list['x-1'] = static function (): A16 { return new A16(); }; +$list['x-2'] = static function (): B16 { return new B16(); }; +$list['x-3'] = static function (): C16 { return new C16(); }; +$list['x-4'] = static function (): D16 { return new D16(); }; +$list['x-5'] = static function (): E16 { return new E16(); }; +$list['x-6'] = static function (): F16 { return new F16(); }; +// +$list['y-1'] = static function (): A16 { return new A16(); }; +$list['y-2'] = static function (): B16 { return new B16(); }; +$list['y-3'] = static function (): C16 { return new C16(); }; +$list['y-4'] = static function (): D16 { return new D16(); }; +$list['y-5'] = static function (): E16 { return new E16(); }; +$list['y-6'] = static function (): F16 { return new F16(); }; +// +$list['z-1'] = static function (): A16 { return new A16(); }; +$list['z-2'] = static function (): B16 { return new B16(); }; +$list['z-3'] = static function (): C16 { return new C16(); }; +$list['z-4'] = static function (): D16 { return new D16(); }; +$list['z-5'] = static function (): E16 { return new E16(); }; +$list['z-6'] = static function (): F16 { return new F16(); }; print 1; From 4de41dee37c4d1157a384c8ffb77154fa04cb179 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 31 Dec 2025 11:56:17 +0100 Subject: [PATCH 08/10] Create degrade-closures.php --- .../Analyser/nsrt/degrade-closures.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/degrade-closures.php diff --git a/tests/PHPStan/Analyser/nsrt/degrade-closures.php b/tests/PHPStan/Analyser/nsrt/degrade-closures.php new file mode 100644 index 0000000000..a310d0d5f3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/degrade-closures.php @@ -0,0 +1,26 @@ +&oversized-array', $arr); From 72cfd58ad2971379217692a2ea9866188ce5bcf3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 31 Dec 2025 11:57:19 +0100 Subject: [PATCH 09/10] Update ConstantArrayTypeBuilder.php --- src/Type/Constant/ConstantArrayTypeBuilder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index 28df23f4ea..d6c602deb6 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -31,7 +31,7 @@ final class ConstantArrayTypeBuilder { public const ARRAY_COUNT_LIMIT = 256; - private const ARRAY_CLOSURES_COUNT_LIMIT = 16; + private const CLOSURES_COUNT_LIMIT = 16; private bool $degradeToGeneralArray = false; @@ -94,7 +94,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt $numClosures++; } - if ($numClosures >= self::ARRAY_CLOSURES_COUNT_LIMIT) { + if ($numClosures >= self::CLOSURES_COUNT_LIMIT) { $this->degradeClosures = true; $this->degradeToGeneralArray = true; $this->oversized = true; From 79b13dd9698a6ea9b97f22fcb97c4d0923aee533 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 31 Dec 2025 12:15:44 +0100 Subject: [PATCH 10/10] simplify --- src/Type/Constant/ConstantArrayTypeBuilder.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index d6c602deb6..fcefe266fb 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -313,15 +313,17 @@ public function getArray(): Type return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } - $itemTypes = []; - foreach ($this->valueTypes as $valueType) { - if ($this->degradeClosures && $valueType instanceof ClosureType) { - continue; - } - $itemTypes[] = $valueType; - } if ($this->degradeClosures) { + $itemTypes = []; $itemTypes[] = new CallableType(); + foreach ($this->valueTypes as $valueType) { + if ($valueType instanceof ClosureType) { + continue; + } + $itemTypes[] = $valueType; + } + } else { + $itemTypes = $this->valueTypes; } $array = new ArrayType(