From 1cc37c6fb7658ad605704fdbf6fed317d47434e8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 9 Sep 2025 14:21:53 +0200 Subject: [PATCH 1/4] For loop fix --- src/Analyser/NodeScopeResolver.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index c6420d1960..80f98fc06b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1494,6 +1494,25 @@ private function processStmtNode( $bodyScope = $condResult->getTruthyScope(); } + if (!$context->isTopLevel() && $isIterableAtLeastOnce->no()) { + if (!isset($condResult)) { + throw new ShouldNotHappenException(); + } + if ($this->polluteScopeWithLoopInitialAssignments) { + $finalScope = $condResult->getFalseyScope()->mergeWith($initScope); + } else { + $finalScope = $condResult->getFalseyScope()->mergeWith($scope); + } + return new StatementResult( + $finalScope, + $hasYield, + false, + [], + $throwPoints, + $impurePoints, + ); + } + if ($context->isTopLevel()) { $count = 0; do { From cfacaa3284c1c9d4ac4b7a4d4d8f1b1d5700ab49 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 9 Sep 2025 14:27:24 +0200 Subject: [PATCH 2/4] Foreach loop fix --- src/Analyser/NodeScopeResolver.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 80f98fc06b..26ce05ba11 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1202,6 +1202,7 @@ private function processStmtNode( $condResult = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep(), $context); $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); $impurePoints = $condResult->getImpurePoints(); + $exprType = $scope->getType($stmt->expr); $scope = $condResult->getScope(); $arrayComparisonExpr = new BinaryOp\NotIdentical( $stmt->expr, @@ -1214,6 +1215,18 @@ private function processStmtNode( $originalScope = $scope; $bodyScope = $scope; + $isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce(); + if (!$context->isTopLevel() && $isIterableAtLeastOnce->no()) { + return new StatementResult( + $scope, + $condResult->hasYield(), + $condResult->isAlwaysTerminating(), + [], + $throwPoints, + $impurePoints, + ); + } + if ($context->isTopLevel()) { $originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope; $bodyScope = $this->enterForeach($originalScope, $originalScope, $stmt); @@ -1250,8 +1263,6 @@ private function processStmtNode( $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); } - $exprType = $scope->getType($stmt->expr); - $isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce(); if ($exprType->isIterable()->no() || $isIterableAtLeastOnce->maybe()) { $finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr( new BinaryOp\Identical( From e48a1a99cdb2e3ba5ab238e3c571772bea445807 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 9 Sep 2025 14:32:08 +0200 Subject: [PATCH 3/4] If condition improvement --- phpstan-baseline.neon | 6 ------ src/Analyser/NodeScopeResolver.php | 12 +++--------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7422156cb6..d7d295cdb3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -42,12 +42,6 @@ parameters: count: 1 path: src/Analyser/MutatingScope.php - - - message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantBooleanType is error\-prone and deprecated\. Use Type\:\:isTrue\(\) or Type\:\:isFalse\(\) instead\.$#' - identifier: phpstanApi.instanceofType - count: 2 - path: src/Analyser/NodeScopeResolver.php - - message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' identifier: phpstanApi.instanceofType diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 26ce05ba11..c6ab207d51 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -171,7 +171,6 @@ use PHPStan\Type\ClosureType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; @@ -1091,7 +1090,7 @@ private function processStmtNode( $branchScopeStatementResult = $this->processStmtNodes($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback, $context); - if (!$conditionType instanceof ConstantBooleanType || $conditionType->getValue()) { + if (!$conditionType->isTrue()->no()) { $exitPoints = $branchScopeStatementResult->getExitPoints(); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); @@ -1123,13 +1122,8 @@ private function processStmtNode( if ( !$ifAlwaysTrue - && ( - !$lastElseIfConditionIsTrue - && ( - !$elseIfConditionType instanceof ConstantBooleanType - || $elseIfConditionType->getValue() - ) - ) + && !$lastElseIfConditionIsTrue + && !$elseIfConditionType->isTrue()->no() ) { $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); From 5b517d1f834d1017ff48e6168f433199e49a4aa3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 9 Sep 2025 14:36:31 +0200 Subject: [PATCH 4/4] More if improvements --- src/Analyser/NodeScopeResolver.php | 96 +++++++++++++++++------------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index c6ab207d51..36bf3938c0 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1078,7 +1078,7 @@ private function processStmtNode( } } elseif ($stmt instanceof If_) { $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); - $ifAlwaysTrue = $conditionType->isTrue()->yes(); + $ifAlwaysTrue = $conditionType->isTrue(); $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep(), $context); $exitPoints = []; $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); @@ -1088,31 +1088,38 @@ private function processStmtNode( $alwaysTerminating = true; $hasYield = $condResult->hasYield(); - $branchScopeStatementResult = $this->processStmtNodes($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback, $context); - - if (!$conditionType->isTrue()->no()) { - $exitPoints = $branchScopeStatementResult->getExitPoints(); - $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); - $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); - $branchScope = $branchScopeStatementResult->getScope(); - $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? null : $branchScope; - $alwaysTerminating = $branchScopeStatementResult->isAlwaysTerminating(); - if (count($branchScopeStatementResult->getEndStatements()) > 0) { - $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements()); - } elseif (count($stmt->stmts) > 0) { - $endStatements[] = new EndStatementResult($stmt->stmts[count($stmt->stmts) - 1], $branchScopeStatementResult); - } else { - $endStatements[] = new EndStatementResult($stmt, $branchScopeStatementResult); + if ($context->isTopLevel() || !$conditionType->isTrue()->no()) { + $branchScopeStatementResult = $this->processStmtNodes($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback, $context); + + if (!$conditionType->isTrue()->no()) { + $exitPoints = $branchScopeStatementResult->getExitPoints(); + $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); + $branchScope = $branchScopeStatementResult->getScope(); + $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? null : $branchScope; + $alwaysTerminating = $branchScopeStatementResult->isAlwaysTerminating(); + if (count($branchScopeStatementResult->getEndStatements()) > 0) { + $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements()); + } elseif (count($stmt->stmts) > 0) { + $endStatements[] = new EndStatementResult($stmt->stmts[count($stmt->stmts) - 1], $branchScopeStatementResult); + } else { + $endStatements[] = new EndStatementResult($stmt, $branchScopeStatementResult); + } + $hasYield = $branchScopeStatementResult->hasYield() || $hasYield; } - $hasYield = $branchScopeStatementResult->hasYield() || $hasYield; } $scope = $condResult->getFalseyScope(); - $lastElseIfConditionIsTrue = false; + $lastElseIfConditionIsTrue = TrinaryLogic::createNo(); $condScope = $scope; foreach ($stmt->elseifs as $elseif) { $nodeCallback($elseif, $scope); + if (!$context->isTopLevel()) { + if ($ifAlwaysTrue->yes() || $lastElseIfConditionIsTrue->yes()) { + continue; + } + } $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condScope->getType($elseif->cond) : $scope->getNativeType($elseif->cond))->toBoolean(); $condResult = $this->processExprNode($stmt, $elseif->cond, $condScope, $nodeCallback, ExpressionContext::createDeep(), $context); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); @@ -1120,9 +1127,14 @@ private function processStmtNode( $condScope = $condResult->getScope(); $branchScopeStatementResult = $this->processStmtNodes($elseif, $elseif->stmts, $condResult->getTruthyScope(), $nodeCallback, $context); + if (!$context->isTopLevel() && $elseIfConditionType->isTrue()->no()) { + $scope = $condScope->filterByFalseyValue($elseif->cond); + continue; + } + if ( - !$ifAlwaysTrue - && !$lastElseIfConditionIsTrue + !$ifAlwaysTrue->yes() + && !$lastElseIfConditionIsTrue->yes() && !$elseIfConditionType->isTrue()->no() ) { $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); @@ -1141,40 +1153,40 @@ private function processStmtNode( $hasYield = $hasYield || $branchScopeStatementResult->hasYield(); } - if ( - $elseIfConditionType->isTrue()->yes() - ) { - $lastElseIfConditionIsTrue = true; + if ($elseIfConditionType->isTrue()->yes()) { + $lastElseIfConditionIsTrue = $elseIfConditionType->isTrue(); } - $condScope = $condScope->filterByFalseyValue($elseif->cond); $scope = $condScope; } if ($stmt->else === null) { - if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { + if (!$ifAlwaysTrue->yes() && !$lastElseIfConditionIsTrue->yes()) { $finalScope = $scope->mergeWith($finalScope); $alwaysTerminating = false; } } else { $nodeCallback($stmt->else, $scope); - $branchScopeStatementResult = $this->processStmtNodes($stmt->else, $stmt->else->stmts, $scope, $nodeCallback, $context); - if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { - $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); - $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); - $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); - $branchScope = $branchScopeStatementResult->getScope(); - $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope); - $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating(); - if (count($branchScopeStatementResult->getEndStatements()) > 0) { - $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements()); - } elseif (count($stmt->else->stmts) > 0) { - $endStatements[] = new EndStatementResult($stmt->else->stmts[count($stmt->else->stmts) - 1], $branchScopeStatementResult); - } else { - $endStatements[] = new EndStatementResult($stmt->else, $branchScopeStatementResult); + if ($context->isTopLevel() || (!$ifAlwaysTrue->yes() && !$lastElseIfConditionIsTrue->yes())) { + $branchScopeStatementResult = $this->processStmtNodes($stmt->else, $stmt->else->stmts, $scope, $nodeCallback, $context); + + if (!$ifAlwaysTrue->yes() && !$lastElseIfConditionIsTrue->yes()) { + $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); + $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); + $branchScope = $branchScopeStatementResult->getScope(); + $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope); + $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating(); + if (count($branchScopeStatementResult->getEndStatements()) > 0) { + $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements()); + } elseif (count($stmt->else->stmts) > 0) { + $endStatements[] = new EndStatementResult($stmt->else->stmts[count($stmt->else->stmts) - 1], $branchScopeStatementResult); + } else { + $endStatements[] = new EndStatementResult($stmt->else, $branchScopeStatementResult); + } + $hasYield = $hasYield || $branchScopeStatementResult->hasYield(); } - $hasYield = $hasYield || $branchScopeStatementResult->hasYield(); } } @@ -1182,7 +1194,7 @@ private function processStmtNode( $finalScope = $scope; } - if ($stmt->else === null && !$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { + if ($stmt->else === null && !$ifAlwaysTrue->yes() && !$lastElseIfConditionIsTrue->yes()) { $endStatements[] = new EndStatementResult($stmt, new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints)); }