From cc9deaef0802830495e506bcf04ad0cb0e1864c9 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:15:40 +0000 Subject: [PATCH 1/9] Preserve list type when overwriting existing elements via count()-1, array_key_last, array_key_first - Extended list type preservation in AssignHandler to recognize count($list) - n, array_key_last($list), and array_key_first($list) as existing-key offset patterns - Added list preservation in IntersectionType::setExistingOffsetValueType - New regression test in tests/PHPStan/Analyser/nsrt/bug-14245.php Closes https://github.com/phpstan/phpstan/issues/14245 --- src/Analyser/ExprHandler/AssignHandler.php | 67 ++++++++++++++++------ src/Type/IntersectionType.php | 8 ++- tests/PHPStan/Analyser/nsrt/bug-14245.php | 53 +++++++++++++++++ 3 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14245.php diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 89b53c46c2..611f4cc8b0 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -987,25 +987,51 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar continue; } - if (!$arrayDimFetch->dim instanceof Expr\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 + $keepList = false; + if ($arrayDimFetch->dim instanceof Expr\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() + ) { + $keepList = true; + } 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() + ) { + $keepList = true; + } + } elseif ( // keep list for $list[count($list) - n] assignments + $offsetValueType->isIterableAtLeastOnce()->yes() + && $arrayDimFetch->dim instanceof Expr\BinaryOp\Minus && $arrayDimFetch->dim->right instanceof Node\Scalar\Int_ - && $arrayDimFetch->dim->right->value === 1 - && $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes() + && $arrayDimFetch->dim->right->value >= 1 + && $arrayDimFetch->dim->left instanceof Expr\FuncCall + && $arrayDimFetch->dim->left->name instanceof Name + && $arrayDimFetch->dim->left->name->toLowerString() === 'count' + && count($arrayDimFetch->dim->left->getArgs()) >= 1 + && $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->left->getArgs()[0]->value) ) { - $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); + $keepList = true; + } elseif ( // keep list for $list[array_key_last($list)] and $list[array_key_first($list)] assignments + $offsetValueType->isIterableAtLeastOnce()->yes() + && $arrayDimFetch->dim instanceof Expr\FuncCall + && $arrayDimFetch->dim->name instanceof Name + && in_array($arrayDimFetch->dim->name->toLowerString(), ['array_key_last', 'array_key_first'], true) + && count($arrayDimFetch->dim->getArgs()) >= 1 + && $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->getArgs()[0]->value) + ) { + $keepList = true; } + + if (!$keepList) { + continue; + } + + $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); } $additionalExpressions = []; @@ -1029,4 +1055,13 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar return [$valueToWrite, $additionalExpressions]; } + private function isSameVariable(Expr $a, Expr $b): bool + { + if ($a instanceof Variable && $b instanceof Variable && is_string($a->name) && is_string($b->name)) { + return $a->name === $b->name; + } + + return false; + } + } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index bd203c08bd..a4ccf3dcf9 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -986,7 +986,13 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType)); + $result = $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType)); + + if ($this->isList()->yes() && !$result->isList()->yes()) { + $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); + } + + return $result; } public function unsetOffset(Type $offsetType): Type diff --git a/tests/PHPStan/Analyser/nsrt/bug-14245.php b/tests/PHPStan/Analyser/nsrt/bug-14245.php new file mode 100644 index 0000000000..5737396b8f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14245.php @@ -0,0 +1,53 @@ + + */ +function foo(): array { + return []; +} + +function doFoo(): void { + $range = foo(); + $count = count($range); + assertType('list', $range); + if ($count > 0) { + assertType('non-empty-list', $range); + $range[count($range) - 1] = 37; + assertType('non-empty-list', $range); + } + + assertType('list', $range); +} + +function doBar(): void { + $range = foo(); + $count = count($range); + assertType('list', $range); + if ($count > 0) { + assertType('non-empty-list', $range); + $range[array_key_last($range)] = 37; + assertType('non-empty-list', $range); + } + + assertType('list', $range); +} + +function doBaz(): void { + $range = foo(); + $count = count($range); + assertType('list', $range); + if ($count > 0) { + assertType('non-empty-list', $range); + $range[array_key_first($range)] = 37; + assertType('non-empty-list', $range); + } + + assertType('list', $range); +} From 31f0c2a409a84e0bae400ec257a40a1a069828e8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 9 Mar 2026 17:41:40 +0100 Subject: [PATCH 2/9] improve tests --- tests/PHPStan/Analyser/nsrt/bug-14245.php | 55 ++++++++++++++--------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-14245.php b/tests/PHPStan/Analyser/nsrt/bug-14245.php index 5737396b8f..57313ef64e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14245.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14245.php @@ -14,40 +14,53 @@ function foo(): array { } function doFoo(): void { - $range = foo(); - $count = count($range); - assertType('list', $range); + $list = foo(); + $count = count($list); + assertType('list', $list); if ($count > 0) { - assertType('non-empty-list', $range); - $range[count($range) - 1] = 37; - assertType('non-empty-list', $range); + assertType('non-empty-list', $list); + $list[count($list) - 1] = 37; + assertType('non-empty-list', $list); } - assertType('list', $range); + assertType('list', $list); +} + +function doFoo2(): void { + $list = foo(); + $count = count($list); + assertType('list', $list); + if ($count > 0) { + assertType('non-empty-list', $list); + $list[count($list) - 5] = 37; // we don't know the $list length, therefore count() - N might be before the first element + assertType('array', $list); + } + + assertType('list', $list); } function doBar(): void { - $range = foo(); - $count = count($range); - assertType('list', $range); + $list = foo(); + $count = count($list); + assertType('list', $list); if ($count > 0) { - assertType('non-empty-list', $range); - $range[array_key_last($range)] = 37; - assertType('non-empty-list', $range); + assertType('non-empty-list', $list); + $list[array_key_last($list)] = 37; + assertType('non-empty-list', $list); } - assertType('list', $range); + assertType('list', $list); } function doBaz(): void { - $range = foo(); - $count = count($range); - assertType('list', $range); + $list = foo(); + $count = count($list); + assertType('list', $list); if ($count > 0) { - assertType('non-empty-list', $range); - $range[array_key_first($range)] = 37; - assertType('non-empty-list', $range); + assertType('non-empty-list', $list); + $list[array_key_first($list)] = 37; + assertType('non-empty-list', $list); } - assertType('list', $range); + assertType('list', $list); } From 8ee03f7cfadaed617be29638962ba446b0e85878 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 9 Mar 2026 18:10:30 +0100 Subject: [PATCH 3/9] more tests --- src/Analyser/ExprHandler/AssignHandler.php | 12 +++---- tests/PHPStan/Analyser/nsrt/bug-14245.php | 37 +++++++++++++++++++--- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 611f4cc8b0..1d68ea54a8 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -53,6 +53,7 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\ErrorType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\StaticTypeFactory; @@ -1005,20 +1006,19 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $keepList = true; } } elseif ( // keep list for $list[count($list) - n] assignments - $offsetValueType->isIterableAtLeastOnce()->yes() - && $arrayDimFetch->dim instanceof Expr\BinaryOp\Minus + $arrayDimFetch->dim instanceof Expr\BinaryOp\Minus && $arrayDimFetch->dim->right instanceof Node\Scalar\Int_ - && $arrayDimFetch->dim->right->value >= 1 && $arrayDimFetch->dim->left instanceof Expr\FuncCall && $arrayDimFetch->dim->left->name instanceof Name && $arrayDimFetch->dim->left->name->toLowerString() === 'count' - && count($arrayDimFetch->dim->left->getArgs()) >= 1 + && count($arrayDimFetch->dim->left->getArgs()) === 1 // could support COUNT_RECURSIVE, COUNT_NORMAL && $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->left->getArgs()[0]->value) + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($arrayDimFetch->dim))->yes() + && $offsetValueType->isIterableAtLeastOnce()->yes() ) { $keepList = true; } elseif ( // keep list for $list[array_key_last($list)] and $list[array_key_first($list)] assignments - $offsetValueType->isIterableAtLeastOnce()->yes() - && $arrayDimFetch->dim instanceof Expr\FuncCall + $arrayDimFetch->dim instanceof Expr\FuncCall && $arrayDimFetch->dim->name instanceof Name && in_array($arrayDimFetch->dim->name->toLowerString(), ['array_key_last', 'array_key_first'], true) && count($arrayDimFetch->dim->getArgs()) >= 1 diff --git a/tests/PHPStan/Analyser/nsrt/bug-14245.php b/tests/PHPStan/Analyser/nsrt/bug-14245.php index 57313ef64e..f4781e515a 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14245.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14245.php @@ -32,14 +32,28 @@ function doFoo2(): void { assertType('list', $list); if ($count > 0) { assertType('non-empty-list', $list); - $list[count($list) - 5] = 37; // we don't know the $list length, therefore count() - N might be before the first element - assertType('array', $list); + // we don't know the $list length, + // therefore count() - N might be before the first element -> degrade to array + $list[count($list) - 5] = 37; + assertType('non-empty-array, int>', $list); + } + + assertType('array, int>', $list); +} + +function listKnownSize(): void { + $list = foo(); + assertType('list', $list); + if (count($list) === 5) { + assertType('array{int, int, int, int, int}', $list); + $list[count($list) - 3] = 37; + assertType('array{int, int, 37, int, int}', $list); } assertType('list', $list); } -function doBar(): void { +function overwriteKeyLast(): void { $list = foo(); $count = count($list); assertType('list', $list); @@ -52,7 +66,7 @@ function doBar(): void { assertType('list', $list); } -function doBaz(): void { +function overwriteKeyFirst(): void { $list = foo(); $count = count($list); assertType('list', $list); @@ -64,3 +78,18 @@ function doBaz(): void { assertType('list', $list); } + +function overwriteKeyFirstMaybeEmptyArray(): void { + $list = foo(); + assertType('list', $list); + // empty list might return NULL for array_key_first() + $list[array_key_first($list)] = 37; + assertType('non-empty-list', $list); +} + +function keyDifferentArray(array $arr): void { + $list = foo(); + assertType('list', $list); + $list[array_key_first($arr)] = 37; + assertType('non-empty-array', $list); +} From 140d39190d518daa9bae5e2b8a3df3a878cd9078 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Mar 2026 07:04:34 +0100 Subject: [PATCH 4/9] extract method --- src/Analyser/ExprHandler/AssignHandler.php | 84 +++++++++++----------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 1d68ea54a8..4fed2cb9b0 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -988,46 +988,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar continue; } - $keepList = false; - if ($arrayDimFetch->dim instanceof Expr\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() - ) { - $keepList = true; - } 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() - ) { - $keepList = true; - } - } elseif ( // keep list for $list[count($list) - n] assignments - $arrayDimFetch->dim instanceof Expr\BinaryOp\Minus - && $arrayDimFetch->dim->right instanceof Node\Scalar\Int_ - && $arrayDimFetch->dim->left instanceof Expr\FuncCall - && $arrayDimFetch->dim->left->name instanceof Name - && $arrayDimFetch->dim->left->name->toLowerString() === 'count' - && count($arrayDimFetch->dim->left->getArgs()) === 1 // could support COUNT_RECURSIVE, COUNT_NORMAL - && $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->left->getArgs()[0]->value) - && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($arrayDimFetch->dim))->yes() - && $offsetValueType->isIterableAtLeastOnce()->yes() - ) { - $keepList = true; - } elseif ( // keep list for $list[array_key_last($list)] and $list[array_key_first($list)] assignments - $arrayDimFetch->dim instanceof Expr\FuncCall - && $arrayDimFetch->dim->name instanceof Name - && in_array($arrayDimFetch->dim->name->toLowerString(), ['array_key_last', 'array_key_first'], true) - && count($arrayDimFetch->dim->getArgs()) >= 1 - && $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->getArgs()[0]->value) - ) { - $keepList = true; - } - - if (!$keepList) { + if (!$this->shouldKeepList($arrayDimFetch, $scope, $offsetValueType)) { continue; } @@ -1055,6 +1016,49 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar return [$valueToWrite, $additionalExpressions]; } + private function shouldKeepList(ArrayDimFetch $arrayDimFetch, Scope $scope, Type $offsetValueType): bool + { + if ($arrayDimFetch->dim instanceof Expr\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() + ) { + return true; + } 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() + ) { + return true; + } + } elseif ( // keep list for $list[count($list) - n] assignments + $arrayDimFetch->dim instanceof Expr\BinaryOp\Minus + && $arrayDimFetch->dim->right instanceof Node\Scalar\Int_ + && $arrayDimFetch->dim->left instanceof Expr\FuncCall + && $arrayDimFetch->dim->left->name instanceof Name + && $arrayDimFetch->dim->left->name->toLowerString() === 'count' + && count($arrayDimFetch->dim->left->getArgs()) === 1 // could support COUNT_RECURSIVE, COUNT_NORMAL + && $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->left->getArgs()[0]->value) + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($arrayDimFetch->dim))->yes() + && $offsetValueType->isIterableAtLeastOnce()->yes() + ) { + return true; + } elseif ( // keep list for $list[array_key_last($list)] and $list[array_key_first($list)] assignments + $arrayDimFetch->dim instanceof Expr\FuncCall + && $arrayDimFetch->dim->name instanceof Name + && in_array($arrayDimFetch->dim->name->toLowerString(), ['array_key_last', 'array_key_first'], true) + && count($arrayDimFetch->dim->getArgs()) >= 1 + && $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->getArgs()[0]->value) + ) { + return true; + } + + return false; + } + private function isSameVariable(Expr $a, Expr $b): bool { if ($a instanceof Variable && $b instanceof Variable && is_string($a->name) && is_string($b->name)) { From 7126910d557528d7a5db6f5285d228f016ddc726 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Mar 2026 07:05:43 +0100 Subject: [PATCH 5/9] remove ai leftover --- src/Type/IntersectionType.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index a4ccf3dcf9..bd203c08bd 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -986,13 +986,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - $result = $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType)); - - if ($this->isList()->yes() && !$result->isList()->yes()) { - $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); - } - - return $result; + return $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType)); } public function unsetOffset(Type $offsetType): Type From 9a5f88d08e780eb3b70ac5f892b582e340cc8c72 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Mar 2026 07:08:56 +0100 Subject: [PATCH 6/9] same for sizeof --- src/Analyser/ExprHandler/AssignHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 4fed2cb9b0..a42c8ee84c 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -1039,7 +1039,7 @@ private function shouldKeepList(ArrayDimFetch $arrayDimFetch, Scope $scope, Type && $arrayDimFetch->dim->right instanceof Node\Scalar\Int_ && $arrayDimFetch->dim->left instanceof Expr\FuncCall && $arrayDimFetch->dim->left->name instanceof Name - && $arrayDimFetch->dim->left->name->toLowerString() === 'count' + && in_array($arrayDimFetch->dim->left->name->toLowerString(), ['count', 'sizeof'], true) && count($arrayDimFetch->dim->left->getArgs()) === 1 // could support COUNT_RECURSIVE, COUNT_NORMAL && $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->left->getArgs()[0]->value) && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($arrayDimFetch->dim))->yes() From 7c97da4a6cd9d156032a42f45d66d2cb05f06fae Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Mar 2026 10:37:17 +0100 Subject: [PATCH 7/9] array_search() keeps list --- src/Analyser/ExprHandler/AssignHandler.php | 8 ++++++ tests/PHPStan/Analyser/nsrt/bug-14245.php | 30 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index a42c8ee84c..284ec47619 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -1054,6 +1054,14 @@ private function shouldKeepList(ArrayDimFetch $arrayDimFetch, Scope $scope, Type && $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->getArgs()[0]->value) ) { return true; + } elseif ( // keep list for $list[array_search($needle, $list)] assignments + $arrayDimFetch->dim instanceof Expr\FuncCall + && $arrayDimFetch->dim->name instanceof Name + && $arrayDimFetch->dim->name->toLowerString() === 'array_search' + && count($arrayDimFetch->dim->getArgs()) >= 1 + && $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->getArgs()[1]->value) + ) { + return true; } return false; diff --git a/tests/PHPStan/Analyser/nsrt/bug-14245.php b/tests/PHPStan/Analyser/nsrt/bug-14245.php index f4781e515a..728a3fa4a0 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14245.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14245.php @@ -93,3 +93,33 @@ function keyDifferentArray(array $arr): void { $list[array_key_first($arr)] = 37; assertType('non-empty-array', $list); } + +function overwriteArraySearch($needle): void { + $list = foo(); + + assertType('list', $list); + // search in empty-array, or with a non-existent key will return false, + // which gets auto-casted to 0, so we still have a list + // https://3v4l.org/RZbOK + $list[array_search($needle, $list)] = 37; + assertType('non-empty-list', $list); +} + +function overwriteArraySearchStrict($needle): void { + $list = foo(); + + assertType('list', $list); + // search in empty-array, or with a non-existent key will return false, + // which gets auto-casted to 0, so we still have a list + // https://3v4l.org/RZbOK + $list[array_search($needle, $list, true)] = 37; + assertType('non-empty-list', $list); +} + +function ArraySearchWithDifferentArray($array2, $needle): void { + $list = foo(); + + assertType('list', $list); + $list[array_search($needle, $array2, true)] = 37; + assertType('non-empty-array', $list); +} From 39d803de7d8e15d3ff292797da87a4b32281c29e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Mar 2026 10:41:20 +0100 Subject: [PATCH 8/9] test huge list --- tests/PHPStan/Analyser/nsrt/bug-14245.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-14245.php b/tests/PHPStan/Analyser/nsrt/bug-14245.php index 728a3fa4a0..093b0b7ac1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14245.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14245.php @@ -53,6 +53,18 @@ function listKnownSize(): void { assertType('list', $list); } +function listKnownHugeSize(): void { + $list = foo(); + assertType('list', $list); + if (count($list) === 50000) { + assertType('non-empty-list', $list); + $list[count($list) - 3000] = 37; + assertType('non-empty-array, int>', $list); + } + + assertType('array, int>', $list); +} + function overwriteKeyLast(): void { $list = foo(); $count = count($list); From 5c070ed592a17140f63073d480df18f5df563d27 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Mar 2026 10:46:23 +0100 Subject: [PATCH 9/9] test array_key_exists() keeps list --- tests/PHPStan/Analyser/nsrt/bug-14245.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-14245.php b/tests/PHPStan/Analyser/nsrt/bug-14245.php index 093b0b7ac1..63333b5de2 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14245.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14245.php @@ -2,6 +2,7 @@ namespace Bug14245; +use function array_key_exists; use function array_key_first; use function array_key_last; use function PHPStan\Testing\assertType; @@ -135,3 +136,13 @@ function ArraySearchWithDifferentArray($array2, $needle): void { $list[array_search($needle, $array2, true)] = 37; assertType('non-empty-array', $list); } + +function ArrayKeyExistsKeepsList($needle): void { + $list = foo(); + + assertType('list', $list); + if (array_key_exists($needle, $list)) { + $list[$needle] = 37; + } + assertType('list', $list); +}