From 7489b47c8f2bc5b523c68d36f6acd1adc14ac1af Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 3 Oct 2025 09:03:51 +0200 Subject: [PATCH 1/4] 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 e7f26496b8..59c2909771 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -983,7 +983,7 @@ public function testBug7581(): void public function testBug7903(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-7903.php'); - $this->assertCount(23, $errors); + $this->assertCount(28, $errors); } public function testBug7901(): void diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 471a1a1b01..f674646ef6 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1106,6 +1106,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/4] 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 2db90a78a2..f806ea52c9 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1758,13 +1758,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 d520c9f71b1f97bb59ebea9e2906eaad34dc91fc Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 3 Oct 2025 09:49:28 +0200 Subject: [PATCH 3/4] 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 f674646ef6..67141b1b3a 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1117,6 +1117,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 49c080fad4ecfc63f6d5881379d32741b779279b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 6 Oct 2025 19:49:35 +0200 Subject: [PATCH 4/4] 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 67141b1b3a..4aa49f16d9 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 { @@ -919,13 +914,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 @@ -