From e2b7647182f0e9187d19bc7337c69551df54697a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 3 Oct 2025 09:03:51 +0200 Subject: [PATCH 1/5] Report non existent offset on non empty array --- src/Type/TypeUtils.php | 26 ++++++++++++++----- .../Analyser/AnalyserIntegrationTest.php | 2 +- ...nexistentOffsetInArrayDimFetchRuleTest.php | 14 ++++++++++ tests/PHPStan/Rules/Arrays/data/bug-7143.php | 15 +++++++++++ 4 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-7143.php diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index d649cfe0a7..2d3c587294 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use PHPStan\Internal\CombinationsHelper; use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantArrayType; @@ -133,17 +134,28 @@ public static function flattenTypes(Type $type): array return $type->getAllArrays(); } + if ($type instanceof IntersectionType && $type->isConstantArray()->yes()) { + $newTypes = []; + foreach ($type->getTypes() as $innerType) { + $newTypes[] = self::flattenTypes($innerType); + } + + return array_filter( + array_map( + static fn (array $types): Type => TypeCombinator::intersect(...$types), + iterator_to_array(CombinationsHelper::combinations($newTypes)), + ), + static fn (Type $type): bool => !$type instanceof NeverType, + ); + } + if ($type instanceof UnionType) { $types = []; foreach ($type->getTypes() as $innerType) { - if ($innerType instanceof ConstantArrayType) { - foreach ($innerType->getAllArrays() as $array) { - $types[] = $array; - } - continue; + $flattenTypes = self::flattenTypes($innerType); + foreach ($flattenTypes as $flattenType) { + $types[] = $flattenType; } - - $types[] = $innerType; } return $types; diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index a3e9095a89..b7cbf313b8 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -991,7 +991,7 @@ public function testBug7581(): void public function testBug7903(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-7903.php'); - $this->assertCount(24, $errors); + $this->assertCount(29, $errors); } public function testBug7901(): void diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 879c86f791..05c4cd1ba1 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1116,6 +1116,20 @@ public function testPR4385Bis(): void $this->analyse([__DIR__ . '/data/pr-4385-bis.php'], []); } + public function testBug7143(): void + { + $this->analyse([__DIR__ . '/data/bug-7143.php'], [ + [ + "Offset 'foo' might not exist on non-empty-array{foo?: string, bar?: string}.", + 12, + ], + [ + "Offset 'bar' might not exist on non-empty-array{foo?: string, bar?: string}.", + 13, + ], + ]); + } + public function testBug12805(): void { $this->reportPossiblyNonexistentGeneralArrayOffset = true; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-7143.php b/tests/PHPStan/Rules/Arrays/data/bug-7143.php new file mode 100644 index 0000000000..0168fb8094 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-7143.php @@ -0,0 +1,15 @@ + Date: Fri, 3 Oct 2025 09:19:32 +0200 Subject: [PATCH 2/5] Fix --- phpstan-baseline.neon | 4 ++-- src/Internal/CombinationsHelper.php | 3 ++- src/Type/TypeUtils.php | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7caa59f81f..f9de4d0c69 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1812,13 +1812,13 @@ parameters: - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.' identifier: phpstanApi.instanceofType - count: 2 + count: 1 path: src/Type/TypeUtils.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType - count: 3 + count: 4 path: src/Type/TypeUtils.php - diff --git a/src/Internal/CombinationsHelper.php b/src/Internal/CombinationsHelper.php index 48749cac4e..303b8b0a69 100644 --- a/src/Internal/CombinationsHelper.php +++ b/src/Internal/CombinationsHelper.php @@ -2,6 +2,7 @@ namespace PHPStan\Internal; +use Traversable; use function array_shift; final class CombinationsHelper @@ -9,7 +10,7 @@ final class CombinationsHelper /** * @param array> $arrays - * @return iterable> + * @return Traversable> */ public static function combinations(array $arrays): iterable { diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index 2d3c587294..8bcba55c9e 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -10,7 +10,10 @@ use PHPStan\Type\Generic\TemplateBenevolentUnionType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateUnionType; +use function array_filter; +use function array_map; use function array_merge; +use function iterator_to_array; /** * @api From 5a7bde3cf46092c12c4289112f52beb5fa4af278 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 3 Oct 2025 09:49:28 +0200 Subject: [PATCH 3/5] Fix non empty array with lot of optional keys --- src/Type/Constant/ConstantArrayType.php | 2 ++ .../Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php | 8 ++++++++ tests/PHPStan/Rules/Arrays/data/bug-7143.php | 9 +++++++++ 3 files changed, 19 insertions(+) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 4cec2bf409..c334db96b3 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -216,6 +216,8 @@ public function getAllArrays(): array } else { $optionalKeysCombinations = [ [], + array_slice($this->optionalKeys, 0, 1, true), + array_slice($this->optionalKeys, -1, 1, true), $this->optionalKeys, ]; } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 05c4cd1ba1..01228a2924 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1127,6 +1127,14 @@ public function testBug7143(): void "Offset 'bar' might not exist on non-empty-array{foo?: string, bar?: string}.", 13, ], + [ + "Offset 'foo' might not exist on non-empty-array{foo?: string, bar?: string, 1?: 1, 2?: 2, 3?: 3, 4?: 4, 5?: 5, 6?: 6, ...}.", + 21, + ], + [ + "Offset 'bar' might not exist on non-empty-array{foo?: string, bar?: string, 1?: 1, 2?: 2, 3?: 3, 4?: 4, 5?: 5, 6?: 6, ...}.", + 22, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-7143.php b/tests/PHPStan/Rules/Arrays/data/bug-7143.php index 0168fb8094..a0cdf89d6d 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-7143.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-7143.php @@ -12,4 +12,13 @@ public function test(array $arr): void echo $arr['foo']; echo $arr['bar']; } + + /** + * @param array{foo?: string, bar?: string, 1?:1, 2?:2, 3?:3, 4?:4, 5?:5, 6?:6, 7?:7, 8?:8, 9?:9}&non-empty-array $arr + */ + public function test2(array $arr): void + { + echo $arr['foo']; + echo $arr['bar']; + } } From 21e650db74ca5ce665b5c658f68262c318a1f2b9 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 6 Oct 2025 19:49:35 +0200 Subject: [PATCH 4/5] Remove wrong tests --- ...nexistentOffsetInArrayDimFetchRuleTest.php | 12 ---------- tests/PHPStan/Rules/Arrays/data/bug-11602.php | 23 ------------------- tests/PHPStan/Rules/Arrays/data/bug-6379.php | 21 ----------------- 3 files changed, 56 deletions(-) delete mode 100644 tests/PHPStan/Rules/Arrays/data/bug-11602.php delete mode 100644 tests/PHPStan/Rules/Arrays/data/bug-6379.php diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 01228a2924..8996557664 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -385,11 +385,6 @@ public function testBug4747(): void $this->analyse([__DIR__ . '/data/bug-4747.php'], []); } - public function testBug6379(): void - { - $this->analyse([__DIR__ . '/data/bug-6379.php'], []); - } - #[RequiresPhp('>= 8.0')] public function testBug4885(): void { @@ -929,13 +924,6 @@ public function testBug4809(): void $this->analyse([__DIR__ . '/data/bug-4809.php'], []); } - public function testBug11602(): void - { - $this->reportPossiblyNonexistentGeneralArrayOffset = true; - - $this->analyse([__DIR__ . '/data/bug-11602.php'], []); - } - public function testBug12593(): void { $this->reportPossiblyNonexistentGeneralArrayOffset = true; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11602.php b/tests/PHPStan/Rules/Arrays/data/bug-11602.php deleted file mode 100644 index 4e1252e5b4..0000000000 --- a/tests/PHPStan/Rules/Arrays/data/bug-11602.php +++ /dev/null @@ -1,23 +0,0 @@ - Date: Fri, 5 Dec 2025 14:26:17 +0100 Subject: [PATCH 5/5] Fix --- tests/PHPStan/Analyser/AnalyserIntegrationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index b7cbf313b8..a2d8a5542f 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -991,7 +991,7 @@ public function testBug7581(): void public function testBug7903(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-7903.php'); - $this->assertCount(29, $errors); + $this->assertCount(39, $errors); } public function testBug7901(): void