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] 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'; + } +}