From 3c433eabbe3b24311a29707dd2c9fbb5e648bfbb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 16 May 2026 17:17:51 +0200 Subject: [PATCH 1/5] Memoize ArrayType->isList() (#5680) --- src/Type/ArrayType.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 4507fc62261..7e7835f966c 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -61,6 +61,8 @@ class ArrayType implements Type private Type $keyType; private ?Type $cachedIterableKeyType = null; + + private ?TrinaryLogic $isList = null; /** @api */ public function __construct(Type $keyType, private Type $itemType) @@ -296,15 +298,19 @@ public function isConstantArray(): TrinaryLogic public function isList(): TrinaryLogic { - if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($this->getKeyType())->no()) { - return TrinaryLogic::createNo(); - } + if ($this->isList === null) { + if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($this->getKeyType())->no()) { + return $this->isList = TrinaryLogic::createNo(); + } - if ($this->getKeyType()->isSuperTypeOf(new ConstantIntegerType(0))->no()) { - return TrinaryLogic::createNo(); + if ($this->getKeyType()->isSuperTypeOf(new ConstantIntegerType(0))->no()) { + return $this->isList = TrinaryLogic::createNo(); + } + + return $this->isList = TrinaryLogic::createMaybe(); } - return TrinaryLogic::createMaybe(); + return $this->isList; } public function isConstantValue(): TrinaryLogic From 89be924447f1749453bc96da1441ed1eb6cc4610 Mon Sep 17 00:00:00 2001 From: Nicolai <245527909+predictor2718@users.noreply.github.com> Date: Sat, 16 May 2026 22:11:33 +0200 Subject: [PATCH 2/5] Use pre-args scope for value types in array_push/array_unshift (#5579) --- src/Analyser/ExprHandler/FuncCallHandler.php | 5 ++-- tests/PHPStan/Analyser/nsrt/bug-13510.php | 25 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13510.php diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index b0877cdf270..8872cf94b87 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -265,6 +265,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } + $scopeBeforeArgs = $scope; $argsResult = $nodeScopeResolver->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallbackForArgs, $context); $scope = $argsResult->getScope(); $hasYield = $argsResult->hasYield(); @@ -395,8 +396,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $stmt, $arrayArg, new NativeTypeExpr( - $this->getArrayFunctionAppendingType($functionReflection, $scope, $normalizedExpr), - $this->getArrayFunctionAppendingType($functionReflection, $scope->doNotTreatPhpDocTypesAsCertain(), $normalizedExpr), + $this->getArrayFunctionAppendingType($functionReflection, $scopeBeforeArgs, $normalizedExpr), + $this->getArrayFunctionAppendingType($functionReflection, $scopeBeforeArgs->doNotTreatPhpDocTypesAsCertain(), $normalizedExpr), ), $nodeCallback, )->getScope(); diff --git a/tests/PHPStan/Analyser/nsrt/bug-13510.php b/tests/PHPStan/Analyser/nsrt/bug-13510.php new file mode 100644 index 00000000000..86405ed71ae --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13510.php @@ -0,0 +1,25 @@ + $arr */ + public function test(array $arr): void + { + array_unshift($arr, array_pop($arr)); + assertType('non-empty-list', $arr); + } + + /** @param non-empty-list $arr */ + public function testTwoLines(array $arr): void + { + $popped = array_pop($arr); + array_unshift($arr, $popped); + assertType('non-empty-list', $arr); + } + +} From 639d82f4e8d847034a77d927f5a21bc79b660a65 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sun, 17 May 2026 09:50:41 +0200 Subject: [PATCH 3/5] Add regression test for circular class constant PHPDoc type references (#5685) Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 --- .../Analyser/AnalyserIntegrationTest.php | 6 +++++ tests/PHPStan/Analyser/data/bug-9172.php | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 tests/PHPStan/Analyser/data/bug-9172.php diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 27b9f44c3de..08b8f097f3a 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1578,6 +1578,12 @@ public function testBug14596(): void $this->assertNotEmpty($errors); } + public function testBug9172(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9172.php'); + $this->assertNotEmpty($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return list diff --git a/tests/PHPStan/Analyser/data/bug-9172.php b/tests/PHPStan/Analyser/data/bug-9172.php new file mode 100644 index 00000000000..0118ca9438d --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9172.php @@ -0,0 +1,26 @@ + */ + public const MIN_DEPOSIT = 1_000; + + /** @var int */ + public const MAX_DEPOSIT = 20_000; + + /** @param int $amount */ + public function deposit(int $amount): void + { + } +} + +final class CircularValues +{ + /** @var int<0, self::MAX> */ + public const MIN = self::MAX - 19_000; + + /** @var int */ + public const MAX = self::MIN + 19_000; +} From a2a28cce9550390bb19b951242a6f8b89e5ab3bc Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sun, 17 May 2026 09:54:56 +0200 Subject: [PATCH 4/5] Use before-scope for evaluating `array_splice` argument types (#5682) Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 --- src/Analyser/ExprHandler/FuncCallHandler.php | 21 ++++++-- tests/PHPStan/Analyser/nsrt/bug-13510.php | 51 ++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) 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); + } } From d042a7fedacc9baad1539e8a1f3b4171f7d621b0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 17 May 2026 10:32:40 +0200 Subject: [PATCH 5/5] cs --- src/Type/ArrayType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 7e7835f966c..0f59c04d316 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -61,7 +61,7 @@ class ArrayType implements Type private Type $keyType; private ?Type $cachedIterableKeyType = null; - + private ?TrinaryLogic $isList = null; /** @api */