From bc3d3c5279c344bb11e57ce197a4bf8eb4f24fd8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 3 Oct 2025 09:03:51 +0200 Subject: [PATCH 1/8] 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 7ef64cac66..5383475b10 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; @@ -134,17 +135,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 cf56a34adc..618d821590 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/8] 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 23b4baa7a4..41406270b9 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1746,13 +1746,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 056fd7f140..d5876a577a 100644 --- a/src/Internal/CombinationsHelper.php +++ b/src/Internal/CombinationsHelper.php @@ -3,13 +3,14 @@ namespace PHPStan\Internal; use function array_pop; +use Traversable; 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 5383475b10..4561988e63 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -11,7 +11,10 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateUnionType; use PHPStan\Type\Traverser\LateResolvableTraverser; +use function array_filter; +use function array_map; use function array_merge; +use function iterator_to_array; /** * @api From 33406a66df2d31462964223a087f2a4d71962205 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 3 Oct 2025 09:49:28 +0200 Subject: [PATCH 3/8] 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 6cff7ef3af..70a30ef567 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 13393a0870c263c1d12069513ad1c482ed0b645e Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 6 Oct 2025 19:49:35 +0200 Subject: [PATCH 4/8] 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/8] 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 618d821590..848881796c 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 From efd51a80598140e5e1d6da4b43b091620839e5bc Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 1 Feb 2026 12:24:31 +0100 Subject: [PATCH 6/8] Fix cs --- src/Internal/CombinationsHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Internal/CombinationsHelper.php b/src/Internal/CombinationsHelper.php index d5876a577a..758b9bab8c 100644 --- a/src/Internal/CombinationsHelper.php +++ b/src/Internal/CombinationsHelper.php @@ -2,8 +2,8 @@ namespace PHPStan\Internal; -use function array_pop; use Traversable; +use function array_pop; final class CombinationsHelper { From 398b701d1a2b28237985b30de499dd29c6f2d854 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 11 Feb 2026 18:20:26 +0100 Subject: [PATCH 7/8] Refactor --- src/Type/TypeUtils.php | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index 4561988e63..2b0c7635de 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -5,7 +5,6 @@ use PHPStan\Internal\CombinationsHelper; use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Accessory\HasPropertyType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Generic\TemplateBenevolentUnionType; use PHPStan\Type\Generic\TemplateType; @@ -134,14 +133,23 @@ public static function toStrictUnion(Type $type): Type */ public static function flattenTypes(Type $type): array { - if ($type instanceof ConstantArrayType) { - return $type->getAllArrays(); + if ($type instanceof UnionType) { + $types = []; + foreach ($type->getTypes() as $innerType) { + $flattenTypes = self::flattenTypes($innerType); + foreach ($flattenTypes as $flattenType) { + $types[] = $flattenType; + } + } + + return $types; } - if ($type instanceof IntersectionType && $type->isConstantArray()->yes()) { + $constantArrays = $type->getConstantArrays(); + if ($constantArrays !== []) { $newTypes = []; - foreach ($type->getTypes() as $innerType) { - $newTypes[] = self::flattenTypes($innerType); + foreach ($constantArrays as $constantArray) { + $newTypes[] = $constantArray->getAllArrays(); } return array_filter( @@ -153,18 +161,6 @@ public static function flattenTypes(Type $type): array ); } - if ($type instanceof UnionType) { - $types = []; - foreach ($type->getTypes() as $innerType) { - $flattenTypes = self::flattenTypes($innerType); - foreach ($flattenTypes as $flattenType) { - $types[] = $flattenType; - } - } - - return $types; - } - return [$type]; } From 99e1fa56c4263c8056e2f79c2e7e336cb79dd440 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 11 Feb 2026 19:31:09 +0100 Subject: [PATCH 8/8] Update baseline --- phpstan-baseline.neon | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 41406270b9..61424ea33d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1743,16 +1743,10 @@ parameters: count: 1 path: src/Type/TypeCombinator.php - - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.' - identifier: phpstanApi.instanceofType - count: 1 - path: src/Type/TypeUtils.php - - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType - count: 4 + count: 3 path: src/Type/TypeUtils.php -