From 3b9d6b26f4c97b316a9e1235cbca94d7a0d8715f Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Sat, 8 Nov 2025 14:21:37 +0100 Subject: [PATCH 1/3] Allow offset in fori loop --- src/Analyser/NodeScopeResolver.php | 14 +++++++++---- tests/PHPStan/Analyser/nsrt/for-loop-expr.php | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 11bd88f402..10dd9a11b7 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -182,6 +182,7 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; @@ -1631,7 +1632,7 @@ private function processStmtNode( if ($lastCondExpr !== null) { $alwaysIterates = $alwaysIterates->and($bodyScope->getType($lastCondExpr)->toBoolean()->isTrue()); $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); - $bodyScope = $this->inferForLoopExpressions($stmt, $lastCondExpr, $bodyScope); + $bodyScope = $this->inferForLoopExpressions($stmt, $lastCondExpr, $bodyScope, $initScope); } $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); @@ -7308,17 +7309,22 @@ private function getFilteringExprForMatchArm(Expr\Match_ $expr, array $condition ); } - private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, MutatingScope $bodyScope): MutatingScope + private function inferForLoopExpressions( + For_ $stmt, + Expr $lastCondExpr, + MutatingScope $bodyScope, + MutatingScope $initScope, + ): MutatingScope { // infer $items[$i] type from for ($i = 0; $i < count($items); $i++) {...} + $positiveInt = IntegerRangeType::fromInterval(0, null); if ( // $i = 0 count($stmt->init) === 1 && $stmt->init[0] instanceof Assign && $stmt->init[0]->var instanceof Variable - && $stmt->init[0]->expr instanceof Node\Scalar\Int_ - && $stmt->init[0]->expr->value === 0 + && $positiveInt->isSuperTypeOf($initScope->getType($stmt->init[0]->expr))->yes() // $i++ or ++$i && count($stmt->loop) === 1 && ($stmt->loop[0] instanceof Expr\PreInc || $stmt->loop[0] instanceof Expr\PostInc) diff --git a/tests/PHPStan/Analyser/nsrt/for-loop-expr.php b/tests/PHPStan/Analyser/nsrt/for-loop-expr.php index 876baaff49..cf092552de 100644 --- a/tests/PHPStan/Analyser/nsrt/for-loop-expr.php +++ b/tests/PHPStan/Analyser/nsrt/for-loop-expr.php @@ -50,3 +50,24 @@ function getItemsArray(array $items): array assertType('array', $items); return $items; } + +/** + * @param array $items + */ +function skipFirstElement(array $items): void +{ + for ($i = 1; count($items) > $i; ++$i) { + $items[$i] = 'hello'; + } +} + +/** + * @param positive-int $skip + * @param array $items + */ +function skipByX(int $skip, array $items): void +{ + for ($i = $skip; count($items) > $i; ++$i) { + $items[$i] = 'hello'; + } +} From d3baf76d6073adbb7cde2b8c28a1aaadd82a1197 Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:24:52 +0100 Subject: [PATCH 2/3] Assert type in tests --- tests/PHPStan/Analyser/nsrt/for-loop-expr.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/for-loop-expr.php b/tests/PHPStan/Analyser/nsrt/for-loop-expr.php index cf092552de..21145cf421 100644 --- a/tests/PHPStan/Analyser/nsrt/for-loop-expr.php +++ b/tests/PHPStan/Analyser/nsrt/for-loop-expr.php @@ -59,6 +59,8 @@ function skipFirstElement(array $items): void for ($i = 1; count($items) > $i; ++$i) { $items[$i] = 'hello'; } + + assertType('array', $items); } /** @@ -70,4 +72,6 @@ function skipByX(int $skip, array $items): void for ($i = $skip; count($items) > $i; ++$i) { $items[$i] = 'hello'; } + + assertType('array', $items); } From e3a8a7ee71c71765e062a78900d924b4fe834e15 Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:51:29 +0100 Subject: [PATCH 3/3] Move test cases for report-possibly-nonexistent-array-offset --- tests/PHPStan/Analyser/nsrt/for-loop-expr.php | 25 ------------------- ...port-possibly-nonexistent-array-offset.php | 23 +++++++++++++++++ 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/for-loop-expr.php b/tests/PHPStan/Analyser/nsrt/for-loop-expr.php index 21145cf421..876baaff49 100644 --- a/tests/PHPStan/Analyser/nsrt/for-loop-expr.php +++ b/tests/PHPStan/Analyser/nsrt/for-loop-expr.php @@ -50,28 +50,3 @@ function getItemsArray(array $items): array assertType('array', $items); return $items; } - -/** - * @param array $items - */ -function skipFirstElement(array $items): void -{ - for ($i = 1; count($items) > $i; ++$i) { - $items[$i] = 'hello'; - } - - assertType('array', $items); -} - -/** - * @param positive-int $skip - * @param array $items - */ -function skipByX(int $skip, array $items): void -{ - for ($i = $skip; count($items) > $i; ++$i) { - $items[$i] = 'hello'; - } - - assertType('array', $items); -} diff --git a/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php index fc54d96f00..618eeb6cd4 100644 --- a/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php +++ b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php @@ -58,3 +58,26 @@ public function nonEmpty(array $a): void } } + +/** + * @param array $items + */ +function skipFirstElement(array $items): void +{ + for ($i = 1; count($items) > $i; ++$i) { + $element = $items[$i]; + \PHPStan\Testing\assertType('string', $element); + } +} + +/** + * @param positive-int $skip + * @param array $items + */ +function skipByX(int $skip, array $items): void +{ + for ($i = $skip; count($items) > $i; ++$i) { + $element = $items[$i]; + \PHPStan\Testing\assertType('string', $element); + } +}