From 3ee5551c5e84eab4bb566532c3dddec682befeb7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 21 Sep 2025 10:20:06 +0200 Subject: [PATCH 01/27] Fix "offset might not exist" false-positives when offset is a expression --- src/Analyser/NodeScopeResolver.php | 20 ++++++++-- ...nexistentOffsetInArrayDimFetchRuleTest.php | 8 ++++ tests/PHPStan/Rules/Arrays/data/bug-13538.php | 38 +++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-13538.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d5f87e3f15..d9a2f61ade 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5705,11 +5705,13 @@ private function processAssignVar( } $offsetValueType = $varType; $offsetNativeValueType = $varNativeType; + $additionalExpressions = []; - $valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope); + $valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope, $additionalExpressions); if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { - $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); + $additionalExpressions = []; + $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope, $additionalExpressions); } else { $rewritten = false; foreach ($offsetTypes as $i => $offsetType) { @@ -5781,6 +5783,12 @@ private function processAssignVar( } } + foreach($additionalExpressions as $additionalExpression) { + [$expr, $type] = $additionalExpression; + + $scope = $scope->assignExpression($expr, $type, $type); + } + if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { $throwPoints = array_merge($throwPoints, $this->processExprNode( $stmt, @@ -6134,8 +6142,9 @@ private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): Tr /** * @param list $dimFetchStack * @param list $offsetTypes + * @param-out array $additionalExpressions */ - private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope): Type + private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope, array &$additionalExpressions = []): Type { $offsetValueTypeStack = [$offsetValueType]; foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { @@ -6170,6 +6179,11 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $offsetValueType = TypeCombinator::intersect($offsetValueType, TypeCombinator::union(...$types)); } + $reverseDimFetch = $dimFetchStack[count($dimFetchStack) - 1 - $i] ?? null; + if ($reverseDimFetch !== null) { + $additionalExpressions[] = [$reverseDimFetch, $valueToWrite]; + } + $arrayDimFetch = $dimFetchStack[$i] ?? null; if ( $offsetType !== null diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 6c79bcc795..2ce7ec2910 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1007,4 +1007,12 @@ public function testBug12926(): void $this->analyse([__DIR__ . '/data/bug-12926.php'], []); } + public function testBug13538(): void + { + $this->reportPossiblyNonexistentConstantArrayOffset = true; + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-13538.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13538.php b/tests/PHPStan/Rules/Arrays/data/bug-13538.php new file mode 100644 index 0000000000..2dbe44b702 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-13538.php @@ -0,0 +1,38 @@ + $arr */ +function doFoo(array $arr, string $s): void +{ + $logs = []; + $logs[$s] = ''; + foreach ($arr as $value) { + echo $logs[$s]; + } +} + +/** @param list $arr */ +function doFooBar(array $arr): void +{ + if (!defined('LOG_DIR')) { + throw new LogicException(); + } + + $logs = []; + $logs[LOG_DIR] = ''; + foreach ($arr as $value) { + echo $logs[LOG_DIR]; + } +} + +function doBar(array $arr, int $i, string $s): void +{ + $logs = []; + $logs[$i][$s] = ''; + foreach ($arr as $value) { + echo $logs[$i][$s]; + } +} From cc0b5f4f23cf0469cada1bff4ca00de582f013e4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Sep 2025 08:45:34 +0200 Subject: [PATCH 02/27] assert types --- .../Analyser/NodeScopeResolverTest.php | 1 + ...nexistentOffsetInArrayDimFetchRuleTest.php | 31 +++++++++++++++- tests/PHPStan/Rules/Arrays/data/bug-13538.php | 36 ++++++++++++++++--- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 587cff9ffa..80984358d1 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -98,6 +98,7 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Generics/data/bug-3769.php'; yield __DIR__ . '/../Rules/Generics/data/bug-6301.php'; yield __DIR__ . '/../Rules/PhpDoc/data/bug-4643.php'; + yield __DIR__ . '/../Rules/Arrays/data/bug-13538.php'; if (PHP_VERSION_ID >= 80000) { yield __DIR__ . '/../Rules/Comparison/data/bug-4857.php'; diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 2ce7ec2910..2303cf2627 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1012,7 +1012,36 @@ public function testBug13538(): void $this->reportPossiblyNonexistentConstantArrayOffset = true; $this->reportPossiblyNonexistentGeneralArrayOffset = true; - $this->analyse([__DIR__ . '/data/bug-13538.php'], []); + $this->analyse([__DIR__ . '/data/bug-13538.php'], [ + [ + "Offset int might not exist on non-empty-array.", + 13, + ], + [ + "Offset int might not exist on non-empty-array.", + 17, + ], + [ + "Offset int might not exist on non-empty-array.", + 21, + ], + [ + "Offset int might not exist on non-empty-array.", + 25, + ], + [ + "Offset int might not exist on non-empty-array.", + 41, + ], + [ + "Offset int might not exist on non-empty-array.", + 45, + ], + [ + "Offset int might not exist on non-empty-array.", + 49, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13538.php b/tests/PHPStan/Rules/Arrays/data/bug-13538.php index 2dbe44b702..6d899f25e3 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-13538.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-13538.php @@ -3,19 +3,31 @@ namespace Bug13538; use LogicException; +use function PHPStan\Testing\assertType; /** @param list $arr */ -function doFoo(array $arr, string $s): void +function doFoo(array $arr, int $i, int $i2): void { $logs = []; - $logs[$s] = ''; + $logs[$i] = ''; + echo $logs[$i2]; + + assertType("non-empty-array", $logs); + assertType("''", $logs[$i]); + assertType("string", $logs[$i2]); + foreach ($arr as $value) { - echo $logs[$s]; + echo $logs[$i]; + echo $logs[$i2]; + + assertType("non-empty-array", $logs); + assertType("''", $logs[$i]); + assertType("string", $logs[$i2]); } } /** @param list $arr */ -function doFooBar(array $arr): void +function doFooBar(array $arr, int $i): void { if (!defined('LOG_DIR')) { throw new LogicException(); @@ -23,8 +35,18 @@ function doFooBar(array $arr): void $logs = []; $logs[LOG_DIR] = ''; + + assertType("non-empty-array<''>", $logs); + assertType("''", $logs[LOG_DIR]); + assertType("string", $logs[$i]); + foreach ($arr as $value) { echo $logs[LOG_DIR]; + echo $logs[$i]; + + assertType("non-empty-array<''>", $logs); + assertType("''", $logs[LOG_DIR]); + assertType("string", $logs[$i]); } } @@ -32,7 +54,13 @@ function doBar(array $arr, int $i, string $s): void { $logs = []; $logs[$i][$s] = ''; + assertType("non-empty-array>", $logs); + assertType("non-empty-array", $logs[$i]); + assertType("''", $logs[$i][$s]); foreach ($arr as $value) { + assertType("non-empty-array>", $logs); + assertType("non-empty-array", $logs[$i]); + assertType("''", $logs[$i][$s]); echo $logs[$i][$s]; } } From b17030a588133e93874a0172176097b502d698b9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Sep 2025 10:17:49 +0200 Subject: [PATCH 03/27] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d9a2f61ade..615cb52aec 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6180,7 +6180,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } $reverseDimFetch = $dimFetchStack[count($dimFetchStack) - 1 - $i] ?? null; - if ($reverseDimFetch !== null) { + if ($reverseDimFetch !== null && $reverseDimFetch->dim !== null) { $additionalExpressions[] = [$reverseDimFetch, $valueToWrite]; } From ffe490af0137a6a082970349dc3077e3e5f7ac43 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Sep 2025 10:41:23 +0200 Subject: [PATCH 04/27] expect invalidation --- .../Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php | 8 ++++++++ tests/PHPStan/Rules/Arrays/data/bug-13538.php | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 2303cf2627..f51bad66d0 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1041,6 +1041,14 @@ public function testBug13538(): void "Offset int might not exist on non-empty-array.", 49, ], + [ + "Offset string might not exist on non-empty-array.", + 68, + ], + [ + "Offset int might not exist on non-empty-array>.", + 68, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13538.php b/tests/PHPStan/Rules/Arrays/data/bug-13538.php index 6d899f25e3..e2efcdfefd 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-13538.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-13538.php @@ -62,5 +62,9 @@ function doBar(array $arr, int $i, string $s): void assertType("non-empty-array", $logs[$i]); assertType("''", $logs[$i][$s]); echo $logs[$i][$s]; + + $i++; + + echo $logs[$i][$s]; } } From f38c6ca6a275631c303f66de2de62a66eb8be785 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Sep 2025 10:47:49 +0200 Subject: [PATCH 05/27] fix --- src/Analyser/NodeScopeResolver.php | 2 +- tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php | 2 +- tests/PHPStan/Rules/Arrays/data/bug-13538.php | 9 ++------- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 615cb52aec..3642d1a3f0 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5783,7 +5783,7 @@ private function processAssignVar( } } - foreach($additionalExpressions as $additionalExpression) { + foreach ($additionalExpressions as $additionalExpression) { [$expr, $type] = $additionalExpression; $scope = $scope->assignExpression($expr, $type, $type); diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 22f21b50a2..c435b35dcb 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -7906,7 +7906,7 @@ public static function dataArrayKeysInBranches(): array '$arrayAppendedInForeach', ], [ - 'non-empty-array, literal-string&lowercase-string&non-falsy-string>', // could be 'array, \'bar\'|\'baz\'|\'foo\'>' + "non-empty-array, 'bar'|'baz'|'foo'>", '$anotherArrayAppendedInForeach', ], [ diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13538.php b/tests/PHPStan/Rules/Arrays/data/bug-13538.php index e2efcdfefd..6ff1a7631a 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-13538.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-13538.php @@ -14,20 +14,18 @@ function doFoo(array $arr, int $i, int $i2): void assertType("non-empty-array", $logs); assertType("''", $logs[$i]); - assertType("string", $logs[$i2]); + assertType("''", $logs[$i2]); // could be mixed foreach ($arr as $value) { echo $logs[$i]; - echo $logs[$i2]; assertType("non-empty-array", $logs); assertType("''", $logs[$i]); - assertType("string", $logs[$i2]); } } /** @param list $arr */ -function doFooBar(array $arr, int $i): void +function doFooBar(array $arr): void { if (!defined('LOG_DIR')) { throw new LogicException(); @@ -38,15 +36,12 @@ function doFooBar(array $arr, int $i): void assertType("non-empty-array<''>", $logs); assertType("''", $logs[LOG_DIR]); - assertType("string", $logs[$i]); foreach ($arr as $value) { echo $logs[LOG_DIR]; - echo $logs[$i]; assertType("non-empty-array<''>", $logs); assertType("''", $logs[LOG_DIR]); - assertType("string", $logs[$i]); } } From eadba0cb0622db956a6376e32879f1945a8fe002 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Sep 2025 17:37:37 +0200 Subject: [PATCH 06/27] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 3642d1a3f0..00bc71aef6 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6179,11 +6179,6 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $offsetValueType = TypeCombinator::intersect($offsetValueType, TypeCombinator::union(...$types)); } - $reverseDimFetch = $dimFetchStack[count($dimFetchStack) - 1 - $i] ?? null; - if ($reverseDimFetch !== null && $reverseDimFetch->dim !== null) { - $additionalExpressions[] = [$reverseDimFetch, $valueToWrite]; - } - $arrayDimFetch = $dimFetchStack[$i] ?? null; if ( $offsetType !== null @@ -6239,6 +6234,13 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } } + $iterableValueType = $valueToWrite->getIterableValueType(); + foreach($dimFetchStack as $dimFetch) { + $additionalExpressions[] = [$dimFetch, $iterableValueType]; + + $iterableValueType = $iterableValueType->getIterableValueType(); + } + return $valueToWrite; } From df2ab0ad221793d51c644d71748e15c50fb39505 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Sep 2025 17:40:38 +0200 Subject: [PATCH 07/27] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 00bc71aef6..d07cff20aa 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6236,6 +6236,11 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $iterableValueType = $valueToWrite->getIterableValueType(); foreach($dimFetchStack as $dimFetch) { + if ($dimFetch->dim === null) { + $additionalExpressions = []; + break; + } + $additionalExpressions[] = [$dimFetch, $iterableValueType]; $iterableValueType = $iterableValueType->getIterableValueType(); From 5699d4a9464991a77a16577204d20996a8b55954 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Sep 2025 17:47:34 +0200 Subject: [PATCH 08/27] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d07cff20aa..ad48596bbc 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6142,7 +6142,7 @@ private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): Tr /** * @param list $dimFetchStack * @param list $offsetTypes - * @param-out array $additionalExpressions + * @param-out list $additionalExpressions */ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope, array &$additionalExpressions = []): Type { @@ -6236,7 +6236,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $iterableValueType = $valueToWrite->getIterableValueType(); foreach($dimFetchStack as $dimFetch) { - if ($dimFetch->dim === null) { + if ($dimFetch->dim === null || $dimFetch->dim instanceof Node\Scalar) { $additionalExpressions = []; break; } From eba597c918cd032f857c7aa52a2f72aaefcd7002 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Sep 2025 19:08:42 +0200 Subject: [PATCH 09/27] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index ad48596bbc..a2efd6cae1 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6142,7 +6142,7 @@ private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): Tr /** * @param list $dimFetchStack * @param list $offsetTypes - * @param-out list $additionalExpressions + * @param list $additionalExpressions */ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope, array &$additionalExpressions = []): Type { @@ -6234,16 +6234,23 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } } - $iterableValueType = $valueToWrite->getIterableValueType(); - foreach($dimFetchStack as $dimFetch) { - if ($dimFetch->dim === null || $dimFetch->dim instanceof Node\Scalar) { - $additionalExpressions = []; - break; - } + if (count($dimFetchStack) > 1) { + $offsetValueType = $valueToWrite; + foreach ($dimFetchStack as $dimFetch) { + if ($dimFetch->dim === null) { + $additionalExpressions = []; + break; + } - $additionalExpressions[] = [$dimFetch, $iterableValueType]; + $offsetType = $scope->getType($dimFetch->dim); + if (!$offsetValueType->hasOffsetValueType($offsetType)->yes()) { + $additionalExpressions = []; + break; + } - $iterableValueType = $iterableValueType->getIterableValueType(); + $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); + $additionalExpressions[] = [$dimFetch, $offsetValueType]; + } } return $valueToWrite; From a3f0dd002fb618d09877c4fcf5fc7031d633dbfe Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Sep 2025 19:09:03 +0200 Subject: [PATCH 10/27] Discard changes to tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php --- tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index c435b35dcb..22f21b50a2 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -7906,7 +7906,7 @@ public static function dataArrayKeysInBranches(): array '$arrayAppendedInForeach', ], [ - "non-empty-array, 'bar'|'baz'|'foo'>", + 'non-empty-array, literal-string&lowercase-string&non-falsy-string>', // could be 'array, \'bar\'|\'baz\'|\'foo\'>' '$anotherArrayAppendedInForeach', ], [ From 6483f7c54be11dd59447d7b7b729b67639e9f9c1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Sep 2025 19:45:19 +0200 Subject: [PATCH 11/27] fix --- src/Analyser/NodeScopeResolver.php | 17 +++++++---- ...nexistentOffsetInArrayDimFetchRuleTest.php | 28 ------------------- tests/PHPStan/Rules/Arrays/data/bug-13538.php | 4 --- 3 files changed, 11 insertions(+), 38 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index a2efd6cae1..4c0fbe7b14 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6146,6 +6146,8 @@ private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): Tr */ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope, array &$additionalExpressions = []): Type { + $originalValueToWrite = $valueToWrite; + $offsetValueTypeStack = [$offsetValueType]; foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { if ($offsetType === null) { @@ -6234,7 +6236,12 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } } - if (count($dimFetchStack) > 1) { + if (count($dimFetchStack) === 1) { + $dimFetch = $dimFetchStack[0]; + if ($dimFetch->dim !== null) { + $additionalExpressions[] = [$dimFetchStack[0], $originalValueToWrite]; + } + } else { $offsetValueType = $valueToWrite; foreach ($dimFetchStack as $dimFetch) { if ($dimFetch->dim === null) { @@ -6243,12 +6250,10 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } $offsetType = $scope->getType($dimFetch->dim); - if (!$offsetValueType->hasOffsetValueType($offsetType)->yes()) { - $additionalExpressions = []; - break; - } - $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); + if ($offsetValueType instanceof ErrorType) { + $offsetValueType = new ConstantArrayType([], []); + } $additionalExpressions[] = [$dimFetch, $offsetValueType]; } } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index f51bad66d0..40a57ac8f6 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1021,34 +1021,6 @@ public function testBug13538(): void "Offset int might not exist on non-empty-array.", 17, ], - [ - "Offset int might not exist on non-empty-array.", - 21, - ], - [ - "Offset int might not exist on non-empty-array.", - 25, - ], - [ - "Offset int might not exist on non-empty-array.", - 41, - ], - [ - "Offset int might not exist on non-empty-array.", - 45, - ], - [ - "Offset int might not exist on non-empty-array.", - 49, - ], - [ - "Offset string might not exist on non-empty-array.", - 68, - ], - [ - "Offset int might not exist on non-empty-array>.", - 68, - ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13538.php b/tests/PHPStan/Rules/Arrays/data/bug-13538.php index 6ff1a7631a..d1eec2b8d1 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-13538.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-13538.php @@ -57,9 +57,5 @@ function doBar(array $arr, int $i, string $s): void assertType("non-empty-array", $logs[$i]); assertType("''", $logs[$i][$s]); echo $logs[$i][$s]; - - $i++; - - echo $logs[$i][$s]; } } From 30b25434621387c85a1824b43f0bc565faa60c36 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Sep 2025 20:15:00 +0200 Subject: [PATCH 12/27] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 4c0fbe7b14..d13c2f2a97 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5710,8 +5710,8 @@ private function processAssignVar( $valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope, $additionalExpressions); if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { - $additionalExpressions = []; - $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope, $additionalExpressions); + $additionalNativeExpressions = []; + $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope, $additionalNativeExpressions); } else { $rewritten = false; foreach ($offsetTypes as $i => $offsetType) { @@ -5783,10 +5783,14 @@ private function processAssignVar( } } - foreach ($additionalExpressions as $additionalExpression) { + foreach ($additionalExpressions as $k => $additionalExpression) { [$expr, $type] = $additionalExpression; + $nativeType = $type; + if (isset($additionalNativeExpressions[$k])) { + [, $nativeType] = $additionalNativeExpressions[$k]; + } - $scope = $scope->assignExpression($expr, $type, $type); + $scope = $scope->assignExpression($expr, $type, $nativeType); } if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { From 31f93a7734f59a12c52d389ae0c20d3821d8b5dd Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 26 Sep 2025 20:45:27 +0200 Subject: [PATCH 13/27] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d13c2f2a97..5adc15bc91 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6243,7 +6243,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar if (count($dimFetchStack) === 1) { $dimFetch = $dimFetchStack[0]; if ($dimFetch->dim !== null) { - $additionalExpressions[] = [$dimFetchStack[0], $originalValueToWrite]; + $additionalExpressions[] = [$dimFetch, $originalValueToWrite]; } } else { $offsetValueType = $valueToWrite; From c4a99e5bd9a9253fd3966c6f6506ca871b32456a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Sep 2025 10:51:18 +0200 Subject: [PATCH 14/27] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index af1ab31238..008704829b 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4308,7 +4308,14 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, } $scope = $this; - if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { + if ( + $expr instanceof Expr\ArrayDimFetch + && $expr->dim !== null + && !$expr->dim instanceof Expr\PreInc + && !$expr->dim instanceof Expr\PreDec + && !$expr->dim instanceof Expr\PostDec + && !$expr->dim instanceof Expr\PostInc + ) { $dimType = $scope->getType($expr->dim)->toArrayKey(); if ($dimType->isInteger()->yes() || $dimType->isString()->yes()) { $exprVarType = $scope->getType($expr->var); From dfb486a59741d6cfa6162431786d2f2bebdce272 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Sep 2025 13:40:30 +0200 Subject: [PATCH 15/27] simplify --- src/Analyser/NodeScopeResolver.php | 23 ++++----- .../Rules/Methods/ReturnTypeRuleTest.php | 5 ++ .../Rules/Methods/data/deep-dim-fetch.php | 51 +++++++++++++++++++ 3 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/deep-dim-fetch.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 5adc15bc91..ac014b7b93 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6240,26 +6240,25 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } } - if (count($dimFetchStack) === 1) { - $dimFetch = $dimFetchStack[0]; - if ($dimFetch->dim !== null) { - $additionalExpressions[] = [$dimFetch, $originalValueToWrite]; + $offsetValueType = $valueToWrite; + $lastDimKey = array_key_last($dimFetchStack); + foreach ($dimFetchStack as $key => $dimFetch) { + if ($dimFetch->dim === null) { + $additionalExpressions = []; + break; } - } else { - $offsetValueType = $valueToWrite; - foreach ($dimFetchStack as $dimFetch) { - if ($dimFetch->dim === null) { - $additionalExpressions = []; - break; - } + if ($key === $lastDimKey) { + $offsetValueType = $originalValueToWrite; + } else { $offsetType = $scope->getType($dimFetch->dim); $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); if ($offsetValueType instanceof ErrorType) { $offsetValueType = new ConstantArrayType([], []); } - $additionalExpressions[] = [$dimFetch, $offsetValueType]; } + + $additionalExpressions[] = [$dimFetch, $offsetValueType]; } return $valueToWrite; diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 6d813ea0bf..d0ec4e729a 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1265,4 +1265,9 @@ public function testBug7225(): void $this->analyse([__DIR__ . '/data/bug-7225.php'], []); } + public function testDeepDimFetch(): void + { + $this->analyse([__DIR__ . '/data/deep-dim-fetch.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/deep-dim-fetch.php b/tests/PHPStan/Rules/Methods/data/deep-dim-fetch.php new file mode 100644 index 0000000000..c08bb442e5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/deep-dim-fetch.php @@ -0,0 +1,51 @@ +getNameScopeKey($fileName, $className, $traitName, $functionName); + if (!isset($this->inProcess[$fileName][$nameScopeKey])) { // wrong $fileName due to traits + throw new \RuntimeException(); + } + + if ($this->inProcess[$fileName][$nameScopeKey] === true) { // PHPDoc has cyclic dependency + throw new \RuntimeException(); + } + + if (is_callable($this->inProcess[$fileName][$nameScopeKey])) { + $resolveCallback = $this->inProcess[$fileName][$nameScopeKey]; + $this->inProcess[$fileName][$nameScopeKey] = true; + $this->inProcess[$fileName][$nameScopeKey] = $resolveCallback(); + } + + return $this->inProcess[$fileName][$nameScopeKey]; + } + + private function getNameScopeKey( + ?string $file, + ?string $class, + ?string $trait, + ?string $function, + ): string + { + return ''; + } + +} From ef3d40d132686963b3d26d68ee4e1d51b0c10306 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Sep 2025 14:07:37 +0200 Subject: [PATCH 16/27] added regression test --- ...mpossibleCheckTypeFunctionCallRuleTest.php | 11 ++++++++ .../PHPStan/Rules/Comparison/data/pr-4375.php | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/pr-4375.php diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 087098ec01..8fe24b3a7e 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1121,4 +1121,15 @@ public function testBug7773(): void $this->analyse([__DIR__ . '/data/bug-7773.php'], []); } + public function testPr4375(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/pr-4375.php'], [ + [ + 'Call to function array_key_exists() with string and array will always evaluate to false.', + 14, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/pr-4375.php b/tests/PHPStan/Rules/Comparison/data/pr-4375.php new file mode 100644 index 0000000000..64da49d270 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/pr-4375.php @@ -0,0 +1,27 @@ +get() as $collected) { + foreach ($collected as [$className, $methodName, $classDisplayName]) { + $className = strtolower($className); + + if (!array_key_exists($className, $methods)) { + $methods[$className] = []; + } + $methods[$className][strtolower($methodName)] = $classDisplayName . '::' . $methodName; + } + } + + return []; + } + + private function get(): array { + return []; + } +} From 3519da378a632033ba3f6e65b91e9e18179305ee Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Sep 2025 14:15:48 +0200 Subject: [PATCH 17/27] Added regression test --- .../PHPStan/Rules/Variables/IssetRuleTest.php | 12 +++++++ .../PHPStan/Rules/Variables/data/pr-4374.php | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/pr-4374.php diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index 8ea1fb0443..da52dc925f 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -492,4 +492,16 @@ public function testIssetAfterRememberedConstructor(): void ]); } + public function testPr4374(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/pr-4374.php'], [ + [ + 'Offset string on array in isset() always exists and is not nullable.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/pr-4374.php b/tests/PHPStan/Rules/Variables/data/pr-4374.php new file mode 100644 index 0000000000..8a19e13efd --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/pr-4374.php @@ -0,0 +1,34 @@ +methods[$cacheKey][$methodName])) { + $method = $this->findClassReflectionWithMethod(); + if ($method === null) { + return false; + } + $this->methods[$cacheKey][$methodName] = $method; + } + + return isset($this->methods[$cacheKey][$methodName]); + } + + private function findClassReflectionWithMethod( + ): ?Foo + { + if (rand(0,1)) { + return new Foo(); + } + return null; + } +} From 3fcf2f2002c9af9e40fed2c0a425d150c09e798d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Sep 2025 14:23:37 +0200 Subject: [PATCH 18/27] simplify --- src/Analyser/NodeScopeResolver.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index ac014b7b93..e6a9401676 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6253,9 +6253,6 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } else { $offsetType = $scope->getType($dimFetch->dim); $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); - if ($offsetValueType instanceof ErrorType) { - $offsetValueType = new ConstantArrayType([], []); - } } $additionalExpressions[] = [$dimFetch, $offsetValueType]; From d4227e26270799aade8ccbc8f5ec64f51f03d6c1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Sep 2025 14:25:54 +0200 Subject: [PATCH 19/27] Update pr-4375.php --- tests/PHPStan/Rules/Comparison/data/pr-4375.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Comparison/data/pr-4375.php b/tests/PHPStan/Rules/Comparison/data/pr-4375.php index 64da49d270..26df1dbfa4 100644 --- a/tests/PHPStan/Rules/Comparison/data/pr-4375.php +++ b/tests/PHPStan/Rules/Comparison/data/pr-4375.php @@ -1,6 +1,6 @@ Date: Sat, 27 Sep 2025 14:35:11 +0200 Subject: [PATCH 20/27] added regression test --- .../Rules/Methods/ReturnTypeRuleTest.php | 6 +++ tests/PHPStan/Rules/Methods/data/bug-9494.php | 53 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-9494.php diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index d0ec4e729a..dd51c3df3a 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1270,4 +1270,10 @@ public function testDeepDimFetch(): void $this->analyse([__DIR__ . '/data/deep-dim-fetch.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testBug9494(): void + { + $this->analyse([__DIR__ . '/data/bug-9494.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-9494.php b/tests/PHPStan/Rules/Methods/data/bug-9494.php new file mode 100644 index 0000000000..3e9146ab6b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9494.php @@ -0,0 +1,53 @@ + 0-indexed memoization */ + protected $mem = []; + + public function __construct(public int $limit) { + $this->mem = array_fill(2, $limit, null); + } + + /** + * Calculate fib, 1-indexed + */ + public function fib(int $n): int + { + if ($n < 1 || $n > $this->limit) { + throw new \RangeException(); + } + + if ($n == 1 || $n == 2) { + return 1; + } + + if (is_null($this->mem[$n - 1])) { + $this->mem[$n - 1] = $this->fib($n - 1) + $this->fib($n - 2); + } + + return $this->mem[$n - 1]; // Is always an int at this stage + } + + /** + * Calculate fib, 0-indexed + */ + public function fib0(int $n0): int + { + if ($n0 < 0 || $n0 >= $this->limit) { + throw new \RangeException(); + } + + if ($n0 == 0 || $n0 == 1) { + return 1; + } + + if (is_null($this->mem[$n0])) { + $this->mem[$n0] = $this->fib0($n0 - 1) + $this->fib0($n0 - 2); + } + + return $this->mem[$n0]; + } +} From d4f227617218d09edc4dbf1c97a0b952090dffe5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Sep 2025 14:40:55 +0200 Subject: [PATCH 21/27] added regression test --- tests/PHPStan/Analyser/nsrt/bug-13214.php | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13214.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-13214.php b/tests/PHPStan/Analyser/nsrt/bug-13214.php new file mode 100644 index 0000000000..3145f1d4b5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13214.php @@ -0,0 +1,41 @@ + $array + */ + public function sayHello(ArrayAccess $array): void + { + $child = new stdClass(); + + assert($array[1] === null); + + assertType('null', $array[1]); + + $array[1] = $child; + + assertType(stdClass::class, $array[1]); + } + + /** + * @param array $array + */ + public function sayHelloArray(array $array): void + { + $child = new stdClass(); + + assert(($array[1] ?? null) === null); + + assertType('object|null', $array[1]); + + $array[1] = $child; + + assertType(stdClass::class, $array[1]); + } +} From f5dc4e74552205b1d6b6799e6be69d0d801de147 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Sep 2025 14:50:38 +0200 Subject: [PATCH 22/27] Added regression test --- ...nexistentOffsetInArrayDimFetchRuleTest.php | 7 ++++++ tests/PHPStan/Rules/Arrays/data/bug-12805.php | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-12805.php diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 40a57ac8f6..e5d17aba80 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1024,4 +1024,11 @@ public function testBug13538(): void ]); } + public function testBug12805(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-12805.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12805.php b/tests/PHPStan/Rules/Arrays/data/bug-12805.php new file mode 100644 index 0000000000..f3b49b6153 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12805.php @@ -0,0 +1,23 @@ + $operations + * @return array + */ +function bug(array $operations): array { + $base = []; + + foreach ($operations as $operationName => $operation) { + if (!isset($base[$operationName])) { + $base[$operationName] = []; + } + if (!isset($base[$operationName]['rtx'])) { + $base[$operationName]['rtx'] = 0; + } + $base[$operationName]['rtx'] += $operation['rtx'] ?? 0; + } + + return $base; +} From 6254a18d1179a29b69d9838ba4fa8176eace3471 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Sep 2025 07:47:27 +0200 Subject: [PATCH 23/27] fix test expectation --- .../Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 8fe24b3a7e..c795706ab9 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1124,12 +1124,7 @@ public function testBug7773(): void public function testPr4375(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/pr-4375.php'], [ - [ - 'Call to function array_key_exists() with string and array will always evaluate to false.', - 14, - ], - ]); + $this->analyse([__DIR__ . '/data/pr-4375.php'], []); } } From 37c720a3dbafc0609f41b19af4bb36d19ddca028 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 28 Sep 2025 11:27:08 +0200 Subject: [PATCH 24/27] Fix ArrayType being created with never value type --- src/Analyser/NodeScopeResolver.php | 34 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index e6a9401676..859cec9c78 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6219,24 +6219,24 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar continue; } - if ($scope->hasExpressionType($arrayDimFetch)->yes()) { // keep list for $list[$index] assignments + if (!$arrayDimFetch->dim instanceof BinaryOp\Plus) { + continue; + } + + if ( // keep list for $list[$index + 1] assignments + $arrayDimFetch->dim->right instanceof Variable + && $arrayDimFetch->dim->left instanceof Node\Scalar\Int_ + && $arrayDimFetch->dim->left->value === 1 + && $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->right))->yes() + ) { + $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); + } elseif ( // keep list for $list[1 + $index] assignments + $arrayDimFetch->dim->left instanceof Variable + && $arrayDimFetch->dim->right instanceof Node\Scalar\Int_ + && $arrayDimFetch->dim->right->value === 1 + && $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes() + ) { $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); - } elseif ($arrayDimFetch->dim instanceof BinaryOp\Plus) { - if ( // keep list for $list[$index + 1] assignments - $arrayDimFetch->dim->right instanceof Variable - && $arrayDimFetch->dim->left instanceof Node\Scalar\Int_ - && $arrayDimFetch->dim->left->value === 1 - && $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->right))->yes() - ) { - $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); - } elseif ( // keep list for $list[1 + $index] assignments - $arrayDimFetch->dim->left instanceof Variable - && $arrayDimFetch->dim->right instanceof Node\Scalar\Int_ - && $arrayDimFetch->dim->right->value === 1 - && $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes() - ) { - $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); - } } } From 58c17521f0e627c46b8b2f979e6a9e5f0b8198e0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Sep 2025 12:02:21 +0200 Subject: [PATCH 25/27] Added regression test --- tests/PHPStan/Levels/data/arrayAccess.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/PHPStan/Levels/data/arrayAccess.php b/tests/PHPStan/Levels/data/arrayAccess.php index b77266e0f6..b07e6930b7 100644 --- a/tests/PHPStan/Levels/data/arrayAccess.php +++ b/tests/PHPStan/Levels/data/arrayAccess.php @@ -44,3 +44,17 @@ public function doLorem( } } + +/** + * @return mixed[] + */ +function bug12931():array { + /** @var array> $data */ + $data = []; + $data['attr'] = []; + $data['attr']['first'] = 1; + $data['attr']['second'] = 2; + $data['attr']['third'] = 3; + + return $data; +} From 74eb4471bf03642f0abf5cee7d67aa56774610e0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Sep 2025 12:04:36 +0200 Subject: [PATCH 26/27] Added regression test --- tests/PHPStan/Analyser/nsrt/bug-13039.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13039.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-13039.php b/tests/PHPStan/Analyser/nsrt/bug-13039.php new file mode 100644 index 0000000000..4cf9e3c9de --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13039.php @@ -0,0 +1,21 @@ +> */ + $transactions = []; + + assertType('list>', $transactions); + + foreach (array_keys($transactions) as $k) { + $transactions[$k]['Shares'] = []; + $transactions[$k]['Shares']['Projects'] = []; + $transactions[$k]['Shares']['People'] = []; + } + + assertType('list>', $transactions); +} + From bbad834950c8a5cec119a4c168e34a65a1c922df Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Sep 2025 12:09:05 +0200 Subject: [PATCH 27/27] drop by-ref parameter --- src/Analyser/NodeScopeResolver.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 859cec9c78..6d5d8216a8 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5705,13 +5705,11 @@ private function processAssignVar( } $offsetValueType = $varType; $offsetNativeValueType = $varNativeType; - $additionalExpressions = []; - $valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope, $additionalExpressions); + [$valueToWrite, $additionalExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope); if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { - $additionalNativeExpressions = []; - $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope, $additionalNativeExpressions); + [$nativeValueToWrite, $additionalNativeExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); } else { $rewritten = false; foreach ($offsetTypes as $i => $offsetType) { @@ -5730,7 +5728,7 @@ private function processAssignVar( continue; } - $nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); + [$nativeValueToWrite] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); $rewritten = true; break; } @@ -6146,9 +6144,10 @@ private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): Tr /** * @param list $dimFetchStack * @param list $offsetTypes - * @param list $additionalExpressions + * + * @return array{Type, list} */ - private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope, array &$additionalExpressions = []): Type + private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope): array { $originalValueToWrite = $valueToWrite; @@ -6240,6 +6239,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } } + $additionalExpressions = []; $offsetValueType = $valueToWrite; $lastDimKey = array_key_last($dimFetchStack); foreach ($dimFetchStack as $key => $dimFetch) { @@ -6258,7 +6258,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $additionalExpressions[] = [$dimFetch, $offsetValueType]; } - return $valueToWrite; + return [$valueToWrite, $additionalExpressions]; } private function unwrapAssign(Expr $expr): Expr