From 84aeaa320a526cd659a0a46ece4efc75bb515fd1 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Sat, 16 May 2026 19:26:37 +0000 Subject: [PATCH 1/2] Use before-scope for evaluating `array_push`/`array_unshift`/`array_splice` argument types - When `array_unshift($this->arr, array_pop($this->arr))` was processed, `getArrayFunctionAppendingType` re-evaluated argument types using `$scope->getType()` on the post-args scope. By that point, the inner `array_pop` had already modified `$this->arr` to be possibly empty, causing `array_pop` to be re-evaluated as returning `T|null` instead of `T`. - Pre-compute argument types from `ExpressionResultStorage::findBeforeScope()` which gives the scope that existed when each argument was originally evaluated. Pass pre-computed PHPDoc and native types separately to `getArrayFunctionAppendingType`. - Apply the same fix to `array_splice` handler, which also re-evaluated offset, length, and replacement argument types in the post-args scope. - Probed sort/shuffle/arsort handlers: these only read the array argument itself (not other args from the scope), so they are not affected. --- src/Analyser/ExprHandler/FuncCallHandler.php | 21 ++++++-- tests/PHPStan/Analyser/nsrt/bug-13510.php | 51 +++++++++++++++++++ .../TypesAssignedToPropertiesRuleTest.php | 5 ++ .../Rules/Properties/data/bug-13510.php | 29 +++++++++++ 4 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-13510.php diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 8872cf94b87..5df15e3fd91 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -435,9 +435,22 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $arrayArgType = $scope->getType($arrayArg); $arrayArgNativeType = $scope->getNativeType($arrayArg); - $offsetType = $scope->getType($normalizedExpr->getArgs()[1]->value); - $lengthType = isset($normalizedExpr->getArgs()[2]) ? $scope->getType($normalizedExpr->getArgs()[2]->value) : new NullType(); - $replacementType = isset($normalizedExpr->getArgs()[3]) ? $scope->getType($normalizedExpr->getArgs()[3]->value) : new ConstantArrayType([], []); + $offsetType = $scopeBeforeArgs->getType($normalizedExpr->getArgs()[1]->value); + + if (isset($normalizedExpr->getArgs()[2])) { + $lengthType = $scopeBeforeArgs->getType($normalizedExpr->getArgs()[2]->value); + } else { + $lengthType = new NullType(); + } + + if (isset($normalizedExpr->getArgs()[3])) { + $replacementArg = $normalizedExpr->getArgs()[3]->value; + $replacementType = $scopeBeforeArgs->getType($replacementArg); + $replacementNativeType = $scopeBeforeArgs->getNativeType($replacementArg); + } else { + $replacementType = new ConstantArrayType([], []); + $replacementNativeType = new ConstantArrayType([], []); + } $scope = $nodeScopeResolver->processVirtualAssign( $scope, @@ -446,7 +459,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $arrayArg, new NativeTypeExpr( $arrayArgType->spliceArray($offsetType, $lengthType, $replacementType), - $arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType), + $arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementNativeType), ), $nodeCallback, )->getScope(); diff --git a/tests/PHPStan/Analyser/nsrt/bug-13510.php b/tests/PHPStan/Analyser/nsrt/bug-13510.php index 86405ed71ae..2ddfccfdcdb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13510.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13510.php @@ -21,5 +21,56 @@ public function testTwoLines(array $arr): void array_unshift($arr, $popped); assertType('non-empty-list', $arr); } +} + +class Bar +{ + /** @var array */ + public array $arr = []; + + public function test(): void + { + if (count($this->arr) === 0) { + throw new \Exception(); + } + assertType('non-empty-array', $this->arr); + array_unshift($this->arr, array_pop($this->arr)); + assertType('non-empty-array', $this->arr); + } + + public function testArrayPush(): void + { + if (count($this->arr) === 0) { + throw new \Exception(); + } + array_push($this->arr, array_pop($this->arr)); + assertType('non-empty-array', $this->arr); + } + public function testArrayUnshiftWithArrayShift(): void + { + if (count($this->arr) === 0) { + throw new \Exception(); + } + array_unshift($this->arr, array_shift($this->arr)); + assertType('non-empty-array', $this->arr); + } + + public function testArrayPushWithArrayShift(): void + { + if (count($this->arr) === 0) { + throw new \Exception(); + } + array_push($this->arr, array_shift($this->arr)); + assertType('non-empty-array', $this->arr); + } + + public function testArraySplice(): void + { + if (count($this->arr) === 0) { + throw new \Exception(); + } + array_splice($this->arr, 0, 0, [array_pop($this->arr)]); + assertType('non-empty-array<(int<0, max>|string), int>', $this->arr); + } } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index ab2f29903f1..bba9c6194a8 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -1071,4 +1071,9 @@ public function testBug10749(): void $this->analyse([__DIR__ . '/data/bug-10749.php'], []); } + public function testBug13510(): void + { + $this->analyse([__DIR__ . '/data/bug-13510.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-13510.php b/tests/PHPStan/Rules/Properties/data/bug-13510.php new file mode 100644 index 00000000000..68bc9bab319 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13510.php @@ -0,0 +1,29 @@ + */ + public array $arr = []; + + public function testArrayUnshift(): void { + if (count($this->arr) === 0) { + throw new \Exception('Narrow to non-empty-array'); + } + array_unshift($this->arr, array_pop($this->arr)); + } + + public function testArrayPush(): void { + if (count($this->arr) === 0) { + throw new \Exception('Narrow to non-empty-array'); + } + array_push($this->arr, array_pop($this->arr)); + } + + public function testArraySplice(): void { + if (count($this->arr) === 0) { + throw new \Exception('Narrow to non-empty-array'); + } + array_splice($this->arr, 0, 0, [array_pop($this->arr)]); + } +} From cf1924acae9d25294c871eef47581a4e9335da50 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 16 May 2026 20:48:34 +0000 Subject: [PATCH 2/2] Remove redundant rule test for bug-13510 The NSRT test already covers the type inference regression. The separate rule test is unnecessary. Co-Authored-By: Claude Opus 4.6 --- .../TypesAssignedToPropertiesRuleTest.php | 5 ---- .../Rules/Properties/data/bug-13510.php | 29 ------------------- 2 files changed, 34 deletions(-) delete mode 100644 tests/PHPStan/Rules/Properties/data/bug-13510.php diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index bba9c6194a8..ab2f29903f1 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -1071,9 +1071,4 @@ public function testBug10749(): void $this->analyse([__DIR__ . '/data/bug-10749.php'], []); } - public function testBug13510(): void - { - $this->analyse([__DIR__ . '/data/bug-13510.php'], []); - } - } diff --git a/tests/PHPStan/Rules/Properties/data/bug-13510.php b/tests/PHPStan/Rules/Properties/data/bug-13510.php deleted file mode 100644 index 68bc9bab319..00000000000 --- a/tests/PHPStan/Rules/Properties/data/bug-13510.php +++ /dev/null @@ -1,29 +0,0 @@ - */ - public array $arr = []; - - public function testArrayUnshift(): void { - if (count($this->arr) === 0) { - throw new \Exception('Narrow to non-empty-array'); - } - array_unshift($this->arr, array_pop($this->arr)); - } - - public function testArrayPush(): void { - if (count($this->arr) === 0) { - throw new \Exception('Narrow to non-empty-array'); - } - array_push($this->arr, array_pop($this->arr)); - } - - public function testArraySplice(): void { - if (count($this->arr) === 0) { - throw new \Exception('Narrow to non-empty-array'); - } - array_splice($this->arr, 0, 0, [array_pop($this->arr)]); - } -}