From da751f4af618818bd6cfedfe2862c993ed57c66d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 11 Jul 2025 15:14:25 +0200 Subject: [PATCH 01/25] Fix missing detection of dead code in expressions --- src/Analyser/ExpressionResult.php | 6 ++++ src/Analyser/NodeScopeResolver.php | 8 +++++ .../DeadCode/UnreachableStatementRuleTest.php | 22 ++++++++++++ .../Rules/DeadCode/data/bug-13232a.php | 15 ++++++++ .../Rules/DeadCode/data/bug-13232b.php | 34 +++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-13232a.php create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-13232b.php diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 0a50465169..037093ed46 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -28,6 +28,7 @@ public function __construct( private array $impurePoints, ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, + private bool $isAlwaysTerminating = false, ) { $this->truthyScopeCallback = $truthyScopeCallback; @@ -90,4 +91,9 @@ public function getFalseyScope(): MutatingScope return $this->falseyScope; } + public function isAlwaysTerminating(): bool + { + return $this->isAlwaysTerminating; + } + } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index dbaa69adfb..da0f0c7625 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -842,17 +842,20 @@ private function processStmtNode( } elseif ($stmt instanceof Echo_) { $hasYield = false; $throwPoints = []; + $isAlwaysTerminating = false; foreach ($stmt->exprs as $echoExpr) { $result = $this->processExprNode($stmt, $echoExpr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } $throwPoints = $overridingThrowPoints ?? $throwPoints; $impurePoints = [ new ImpurePoint($scope, $stmt, 'echo', 'echo', true), ]; + return new StatementResult($scope, $hasYield, $isAlwaysTerminating, [], $throwPoints, $impurePoints); } elseif ($stmt instanceof Return_) { if ($stmt->expr !== null) { $result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); @@ -2410,6 +2413,7 @@ public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scop return $this->processExprNode($stmt, $newExpr, $scope, $nodeCallback, $context); } + $isAlwaysTerminating = false; $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $context); if ($expr instanceof Variable) { @@ -2593,6 +2597,7 @@ static function (): void { if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; + $isAlwaysTerminating = $parametersAcceptor->getReturnType() instanceof NeverType; } $result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context); $scope = $result->getScope(); @@ -3302,6 +3307,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $result = $this->processExprNode($stmt, $expr->right, $scope, $nodeCallback, $context->enterDeep()); if ( ($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) && @@ -3313,6 +3319,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } elseif ($expr instanceof Expr\Include_) { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); @@ -3998,6 +4005,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $impurePoints, static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + $isAlwaysTerminating, ); } diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 87e0dd0c11..c98182e6fe 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -240,4 +240,26 @@ public function testMultipleUnreachable(): void ]); } + public function testBug13232a(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232a.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 11, + ], + ]); + } + + public function testBug13232b(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232b.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php new file mode 100644 index 0000000000..943114526f --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php @@ -0,0 +1,15 @@ + Date: Fri, 11 Jul 2025 15:27:15 +0200 Subject: [PATCH 02/25] Support method calls / static calls --- src/Analyser/NodeScopeResolver.php | 2 ++ .../DeadCode/UnreachableStatementRuleTest.php | 15 ++++++++ .../Rules/DeadCode/data/bug-13232c.php | 34 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-13232c.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index da0f0c7625..c22b27eb9e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2854,6 +2854,7 @@ static function (): void { if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; + $isAlwaysTerminating = $parametersAcceptor->getReturnType() instanceof NeverType; } $result = $this->processArgs( @@ -3040,6 +3041,7 @@ static function (): void { if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; + $isAlwaysTerminating = $parametersAcceptor->getReturnType() instanceof NeverType; } $result = $this->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context, $closureBindScope ?? null); $scope = $result->getScope(); diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index c98182e6fe..cd3c95efdf 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -262,4 +262,19 @@ public function testBug13232b(): void ]); } + public function testBug13232c(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232c.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 12, + ], + [ + 'Unreachable statement - code above always terminates.', + 20, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php new file mode 100644 index 0000000000..0a701a265b --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php @@ -0,0 +1,34 @@ +mightReturnNever() + . ' no way'; + + echo 'this will never happen'; + } + + static public function sayStaticHello(): void + { + echo 'Hello, ' . self::staticMightReturnNever() + . ' no way'; + + echo 'this will never happen'; + } + + function mightReturnNever(): never + + { + exit(); + } + + static function staticMightReturnNever(): never + { + exit(); + } + +} From d7241b628181347259e58dd61d93393f2585a176 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 11 Jul 2025 15:53:49 +0200 Subject: [PATCH 03/25] fix assignments --- src/Analyser/NodeScopeResolver.php | 25 ++++++++++++++++--- .../DeadCode/UnreachableStatementRuleTest.php | 11 ++++++++ .../Rules/DeadCode/data/bug-13232d.php | 15 +++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-13232d.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index c22b27eb9e..85931a06d1 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -929,12 +929,14 @@ private function processStmtNode( $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); + if ($earlyTerminationExpr !== null) { return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } - return new StatementResult($scope, $hasYield, false, [], $overridingThrowPoints ?? $throwPoints, $impurePoints); + return new StatementResult($scope, $hasYield, $isAlwaysTerminating, [], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Namespace_) { if ($stmt->name !== null) { $scope = $scope->enterNamespace($stmt->name->toString()); @@ -2466,13 +2468,14 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); if ($expr instanceof AssignRef) { $scope = $scope->exitExpressionAssign($expr->expr); } - return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); + return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints, isAlwaysTerminating: $isAlwaysTerminating); }, true, ); @@ -2480,6 +2483,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $vars = $this->getAssignedVariables($expr->var); if (count($vars) > 0) { $varChangedScope = false; @@ -2511,6 +2515,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $result->hasYield(), $result->getThrowPoints(), $result->getImpurePoints(), + isAlwaysTerminating: $result->isAlwaysTerminating(), ); } @@ -2522,6 +2527,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); if ( ($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) && !$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() @@ -5382,12 +5388,14 @@ private function processAssignVar( $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; $isAssignOp = $assignedExpr instanceof Expr\AssignOp && !$enterExpressionAssign; if ($var instanceof Variable && is_string($var->name)) { $result = $processExprCallback($scope); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); if (in_array($var->name, Scope::SUPERGLOBAL_VARIABLES, true)) { $impurePoints[] = new ImpurePoint($scope, $var, 'superglobal', 'assign to superglobal variable', true); } @@ -5472,6 +5480,7 @@ private function processAssignVar( $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); if ($enterExpressionAssign) { $scope = $scope->exitExpressionAssign($var); @@ -5523,6 +5532,7 @@ private function processAssignVar( $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); $varType = $scope->getType($var); @@ -5628,6 +5638,7 @@ static function (): void { $hasYield = $objectResult->hasYield(); $throwPoints = $objectResult->getThrowPoints(); $impurePoints = $objectResult->getImpurePoints(); + $isAlwaysTerminating = $objectResult->isAlwaysTerminating(); $scope = $objectResult->getScope(); $propertyName = null; @@ -5638,6 +5649,7 @@ static function (): void { $hasYield = $hasYield || $propertyNameResult->hasYield(); $throwPoints = array_merge($throwPoints, $propertyNameResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $propertyNameResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $propertyNameResult->isAlwaysTerminating(); $scope = $propertyNameResult->getScope(); } @@ -5646,6 +5658,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); if ($var->name instanceof Expr && $this->phpVersion->supportsPropertyHooks()) { @@ -5733,6 +5746,7 @@ static function (): void { $hasYield = $propertyNameResult->hasYield(); $throwPoints = $propertyNameResult->getThrowPoints(); $impurePoints = $propertyNameResult->getImpurePoints(); + $isAlwaysTerminating = $propertyNameResult->isAlwaysTerminating(); $scope = $propertyNameResult->getScope(); } @@ -5741,6 +5755,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); if ($propertyName !== null) { @@ -5785,6 +5800,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); foreach ($var->items as $i => $arrayItem) { if ($arrayItem === null) { @@ -5802,6 +5818,7 @@ static function (): void { $hasYield = $hasYield || $keyResult->hasYield(); $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $keyResult->isAlwaysTerminating(); $itemScope = $keyResult->getScope(); } @@ -5809,6 +5826,7 @@ static function (): void { $hasYield = $hasYield || $valueResult->hasYield(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); if ($arrayItem->key === null) { $dimExpr = new Node\Scalar\Int_($i); @@ -5829,6 +5847,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } } elseif ($var instanceof ExistingArrayDimFetch) { $dimFetchStack = []; @@ -5907,7 +5926,7 @@ static function (): void { } } - return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); + return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints, isAlwaysTerminating: $isAlwaysTerminating); } /** diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index cd3c95efdf..3c3a4154d4 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -277,4 +277,15 @@ public function testBug13232c(): void ]); } + public function testBug13232d(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232d.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 11, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232d.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232d.php new file mode 100644 index 0000000000..89c58d0723 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232d.php @@ -0,0 +1,15 @@ + Date: Fri, 11 Jul 2025 15:56:30 +0200 Subject: [PATCH 04/25] fix regression --- src/Analyser/NodeScopeResolver.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 85931a06d1..08ece8ab1d 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2603,7 +2603,8 @@ static function (): void { if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; - $isAlwaysTerminating = $parametersAcceptor->getReturnType() instanceof NeverType; + $returnType = $parametersAcceptor->getReturnType(); + $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); } $result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context); $scope = $result->getScope(); @@ -2860,7 +2861,8 @@ static function (): void { if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; - $isAlwaysTerminating = $parametersAcceptor->getReturnType() instanceof NeverType; + $returnType = $parametersAcceptor->getReturnType(); + $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); } $result = $this->processArgs( @@ -3047,7 +3049,8 @@ static function (): void { if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; - $isAlwaysTerminating = $parametersAcceptor->getReturnType() instanceof NeverType; + $returnType = $parametersAcceptor->getReturnType(); + $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); } $result = $this->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context, $closureBindScope ?? null); $scope = $result->getScope(); From 80fbd7ada34d1dd6d088c94dec0d2b475e304644 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 11 Jul 2025 16:09:48 +0200 Subject: [PATCH 05/25] Fix NullsafeMethodCall --- src/Analyser/NodeScopeResolver.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 08ece8ab1d..3df8c87563 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2933,6 +2933,7 @@ static function (): void { $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + $exprResult->isAlwaysTerminating() ); } elseif ($expr instanceof StaticCall) { $hasYield = false; From de96c4e4e919748e0db981ab8c0a54d87948f028 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 11 Jul 2025 16:22:49 +0200 Subject: [PATCH 06/25] fix build --- src/Analyser/NodeScopeResolver.php | 4 ++-- .../PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 3df8c87563..cc8a59636a 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2933,7 +2933,7 @@ static function (): void { $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), - $exprResult->isAlwaysTerminating() + $exprResult->isAlwaysTerminating(), ); } elseif ($expr instanceof StaticCall) { $hasYield = false; @@ -5804,7 +5804,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); - $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); foreach ($var->items as $i => $arrayItem) { if ($arrayItem === null) { diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 3c3a4154d4..255349c3b8 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -240,6 +241,7 @@ public function testMultipleUnreachable(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug13232a(): void { $this->treatPhpDocTypesAsCertain = false; @@ -251,6 +253,7 @@ public function testBug13232a(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug13232b(): void { $this->treatPhpDocTypesAsCertain = false; @@ -262,6 +265,7 @@ public function testBug13232b(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug13232c(): void { $this->treatPhpDocTypesAsCertain = false; @@ -277,6 +281,7 @@ public function testBug13232c(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug13232d(): void { $this->treatPhpDocTypesAsCertain = false; From f1637407e7374b96ab65ca25644cee13c6d9832e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 11 Jul 2025 16:28:30 +0200 Subject: [PATCH 07/25] test nullsafe --- src/Analyser/NodeScopeResolver.php | 3 +-- tests/PHPStan/Rules/DeadCode/data/bug-13232c.php | 8 ++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index cc8a59636a..9145e750ce 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2932,8 +2932,7 @@ static function (): void { $exprResult->getThrowPoints(), $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - static fn (): MutatingScope => $scope->filterByFalseyValue($expr), - $exprResult->isAlwaysTerminating(), + static fn (): MutatingScope => $scope->filterByFalseyValue($expr) ); } elseif ($expr instanceof StaticCall) { $hasYield = false; diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php index 0a701a265b..1d4b677cca 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php @@ -20,6 +20,14 @@ static public function sayStaticHello(): void echo 'this will never happen'; } + public function sayNullsafeHello(?self $x): void + { + echo 'Hello, ' . $x?->mightReturnNever() + . ' no way'; + + echo 'this might happen, in case $x is null'; + } + function mightReturnNever(): never { From 8728a7c8b60b7821e025269a3b211aa1c7e9e292 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 11 Jul 2025 16:30:53 +0200 Subject: [PATCH 08/25] more tests --- tests/PHPStan/Rules/DeadCode/data/bug-13232c.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php index 1d4b677cca..e72f1bfbb5 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php @@ -28,6 +28,16 @@ public function sayNullsafeHello(?self $x): void echo 'this might happen, in case $x is null'; } + public function sayMaybeHello(): void + { + if (rand(0, 1)) { + echo 'Hello, ' . $this->mightReturnNever() + . ' no way'; + } + + echo 'this might happen'; + } + function mightReturnNever(): never { From 737fcd7dd693f92ebb63ba2439140fa1248c79d3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 11 Jul 2025 16:38:46 +0200 Subject: [PATCH 09/25] fix PHP 7.4 --- tests/PHPStan/Rules/DeadCode/data/bug-13232c.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php index e72f1bfbb5..cb37a7bbca 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug13232c; From a88893250a49e3ba81e184a18983b71180ed29d9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 11 Jul 2025 16:58:38 +0200 Subject: [PATCH 10/25] better names --- tests/PHPStan/Rules/DeadCode/data/bug-13232c.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php index cb37a7bbca..df3cd78db8 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232c.php @@ -6,7 +6,7 @@ final class HelloWorld { public function sayHello(): void { - echo 'Hello, ' . $this->mightReturnNever() + echo 'Hello, ' . $this->returnNever() . ' no way'; echo 'this will never happen'; @@ -14,7 +14,7 @@ public function sayHello(): void static public function sayStaticHello(): void { - echo 'Hello, ' . self::staticMightReturnNever() + echo 'Hello, ' . self::staticReturnNever() . ' no way'; echo 'this will never happen'; @@ -22,7 +22,7 @@ static public function sayStaticHello(): void public function sayNullsafeHello(?self $x): void { - echo 'Hello, ' . $x?->mightReturnNever() + echo 'Hello, ' . $x?->returnNever() . ' no way'; echo 'this might happen, in case $x is null'; @@ -31,20 +31,20 @@ public function sayNullsafeHello(?self $x): void public function sayMaybeHello(): void { if (rand(0, 1)) { - echo 'Hello, ' . $this->mightReturnNever() + echo 'Hello, ' . $this->returnNever() . ' no way'; } echo 'this might happen'; } - function mightReturnNever(): never + function returnNever(): never { exit(); } - static function staticMightReturnNever(): never + static function staticReturnNever(): never { exit(); } From 7f44e81e3ef6bde2632631059680eef49c5329b1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 12 Jul 2025 07:04:17 +0200 Subject: [PATCH 11/25] fix string interpolation --- src/Analyser/NodeScopeResolver.php | 1 + .../Rules/DeadCode/UnreachableStatementRuleTest.php | 4 ++++ tests/PHPStan/Rules/DeadCode/data/bug-13232a.php | 8 ++++++++ 3 files changed, 13 insertions(+) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 9145e750ce..1bcf6eb1d5 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3211,6 +3211,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); } } elseif ($expr instanceof ArrayDimFetch) { diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 255349c3b8..79685a090c 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -250,6 +250,10 @@ public function testBug13232a(): void 'Unreachable statement - code above always terminates.', 11, ], + [ + 'Unreachable statement - code above always terminates.', + 17, + ], ]); } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php index 943114526f..ef3513ec42 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php @@ -10,6 +10,14 @@ public function sayHi(): void . ' no way'; echo 'this will never happen'; } + + public function sayHo(): void + { + echo "Hello, {$this->neverReturnsMethod()} no way"; + echo 'this will never happen'; + } + + function neverReturnsMethod(): never {} } function neverReturns(): never {} From 6799cd020ca5d992c76a6ca0825b68157dc7669d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 12 Jul 2025 07:19:14 +0200 Subject: [PATCH 12/25] fix func args --- src/Analyser/NodeScopeResolver.php | 9 +++++++-- .../DeadCode/UnreachableStatementRuleTest.php | 6 +++++- tests/PHPStan/Rules/DeadCode/data/bug-13232a.php | 14 ++++++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 1bcf6eb1d5..bb14ebc0c5 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2554,6 +2554,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $scope = $nameResult->getScope(); $throwPoints = $nameResult->getThrowPoints(); $impurePoints = $nameResult->getImpurePoints(); + $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); if ( $nameType->isObject()->yes() && $nameType->isCallable()->yes() @@ -2569,6 +2570,7 @@ static function (): void { ); $throwPoints = array_merge($throwPoints, $invokeResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $invokeResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $invokeResult->isAlwaysTerminating(); } elseif ($parametersAcceptor instanceof CallableParametersAcceptor) { $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $expr, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $expr), $parametersAcceptor->getThrowPoints()); if (!$this->implicitThrows) { @@ -2604,13 +2606,14 @@ static function (): void { if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; $returnType = $parametersAcceptor->getReturnType(); - $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); + $isAlwaysTerminating = $isAlwaysTerminating || $returnType instanceof NeverType && $returnType->isExplicit(); } $result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); if ($functionReflection !== null) { $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope); @@ -5012,6 +5015,7 @@ private function processArgs( $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; foreach ($args as $i => $arg) { $assignByReference = false; $parameter = null; @@ -5161,6 +5165,7 @@ private function processArgs( $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->enterDeep()); $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $exprResult->isAlwaysTerminating(); $scope = $exprResult->getScope(); $hasYield = $hasYield || $exprResult->hasYield(); @@ -5285,7 +5290,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { } } - return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); + return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints, isAlwaysTerminating: $isAlwaysTerminating); } /** diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 79685a090c..ca610ef2ef 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -248,12 +248,16 @@ public function testBug13232a(): void $this->analyse([__DIR__ . '/data/bug-13232a.php'], [ [ 'Unreachable statement - code above always terminates.', - 11, + 10, ], [ 'Unreachable statement - code above always terminates.', 17, ], + [ + 'Unreachable statement - code above always terminates.', + 23, + ], ]); } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php index ef3513ec42..bf15ba9133 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php @@ -4,6 +4,12 @@ final class HelloWorld { + public function sayHa(): void + { + echo sprintf("Hello, %s no way", $this->neverReturnsMethod()); + echo 'this will never happen'; + } + public function sayHi(): void { echo 'Hello, ' . neverReturns() @@ -17,7 +23,11 @@ public function sayHo(): void echo 'this will never happen'; } - function neverReturnsMethod(): never {} + function neverReturnsMethod(): never { + exit(); + } +} +function neverReturns(): never { + exit(); } -function neverReturns(): never {} From 127e14229e601145abf132657c8953e39567872c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 12 Jul 2025 07:23:03 +0200 Subject: [PATCH 13/25] fix callables --- src/Analyser/NodeScopeResolver.php | 2 ++ .../Rules/DeadCode/UnreachableStatementRuleTest.php | 4 ++++ tests/PHPStan/Rules/DeadCode/data/bug-13232a.php | 10 ++++++++++ 3 files changed, 16 insertions(+) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index bb14ebc0c5..ef80a4a621 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5180,6 +5180,8 @@ private function processArgs( } $throwPoints = array_merge($throwPoints, $callableThrowPoints); $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $arg->value, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $acceptors[0]->getImpurePoints())); + $returnType = $acceptors[0]->getReturnType(); + $isAlwaysTerminating = $isAlwaysTerminating || ($returnType instanceof NeverType && $returnType->isExplicit()); } } } diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index ca610ef2ef..fd4f0ae21a 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -258,6 +258,10 @@ public function testBug13232a(): void 'Unreachable statement - code above always terminates.', 23, ], + [ + 'Unreachable statement - code above always terminates.', + 32, + ], ]); } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php index bf15ba9133..5a774c74e9 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php @@ -23,6 +23,16 @@ public function sayHo(): void echo 'this will never happen'; } + public function sayHe(): void + { + $callable = function (): never { + exit(); + }; + echo sprintf("Hello, %s no way", $callable); + echo 'this will never happen'; + } + + function neverReturnsMethod(): never { exit(); } From e332c259cd27eccd5c6a615c64c50200d91589bb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 12 Jul 2025 07:54:52 +0200 Subject: [PATCH 14/25] added regression test --- .../Rules/DeadCode/UnreachableStatementRuleTest.php | 12 ++++++++++++ tests/PHPStan/Rules/DeadCode/data/bug-11909.php | 10 ++++++++++ 2 files changed, 22 insertions(+) create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-11909.php diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index fd4f0ae21a..641c976eff 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -241,6 +241,18 @@ public function testMultipleUnreachable(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testBug11909(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-11909.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 10, + ], + ]); + } + #[RequiresPhp('>= 8.1')] public function testBug13232a(): void { diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-11909.php b/tests/PHPStan/Rules/DeadCode/data/bug-11909.php new file mode 100644 index 0000000000..3b83603011 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-11909.php @@ -0,0 +1,10 @@ + Date: Sat, 12 Jul 2025 08:01:55 +0200 Subject: [PATCH 15/25] fix arrays --- src/Analyser/NodeScopeResolver.php | 4 +++- .../Rules/DeadCode/UnreachableStatementRuleTest.php | 4 ++++ tests/PHPStan/Rules/DeadCode/data/bug-13232a.php | 7 +++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index ef80a4a621..ac89aec528 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2935,7 +2935,7 @@ static function (): void { $exprResult->getThrowPoints(), $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - static fn (): MutatingScope => $scope->filterByFalseyValue($expr) + static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } elseif ($expr instanceof StaticCall) { $hasYield = false; @@ -3247,6 +3247,7 @@ static function (): void { $hasYield = $hasYield || $keyResult->hasYield(); $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $keyResult->isAlwaysTerminating(); $scope = $keyResult->getScope(); } @@ -3254,6 +3255,7 @@ static function (): void { $hasYield = $hasYield || $valueResult->hasYield(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); $scope = $valueResult->getScope(); } $nodeCallback(new LiteralArrayNode($expr, $itemNodes), $scope); diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 641c976eff..c3e9823f3f 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -274,6 +274,10 @@ public function testBug13232a(): void 'Unreachable statement - code above always terminates.', 32, ], + [ + 'Unreachable statement - code above always terminates.', + 40, + ], ]); } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php index 5a774c74e9..9aef70effe 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php @@ -32,6 +32,13 @@ public function sayHe(): void echo 'this will never happen'; } + public function sayHuu(): void + { + $x = [ + $this->neverReturnsMethod() + ]; + echo 'this will never happen'; + } function neverReturnsMethod(): never { exit(); From 0b6e5651ca62cf7924d264e2d730d73d004d0f62 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 12 Jul 2025 08:19:34 +0200 Subject: [PATCH 16/25] fix method calls --- src/Analyser/NodeScopeResolver.php | 6 ++-- .../DeadCode/UnreachableStatementRuleTest.php | 18 +++++++++- .../Rules/DeadCode/data/bug-13232a.php | 36 ++++++++++++++++++- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index ac89aec528..9ae9227fb2 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2554,7 +2554,6 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $scope = $nameResult->getScope(); $throwPoints = $nameResult->getThrowPoints(); $impurePoints = $nameResult->getImpurePoints(); - $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); if ( $nameType->isObject()->yes() && $nameType->isCallable()->yes() @@ -2570,7 +2569,7 @@ static function (): void { ); $throwPoints = array_merge($throwPoints, $invokeResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $invokeResult->getImpurePoints()); - $isAlwaysTerminating = $isAlwaysTerminating || $invokeResult->isAlwaysTerminating(); + $isAlwaysTerminating = $invokeResult->isAlwaysTerminating(); } elseif ($parametersAcceptor instanceof CallableParametersAcceptor) { $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $expr, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $expr), $parametersAcceptor->getThrowPoints()); if (!$this->implicitThrows) { @@ -2818,6 +2817,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); if (isset($closureCallScope)) { $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); @@ -2924,6 +2924,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } elseif ($expr instanceof Expr\NullsafeMethodCall) { $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var); $exprResult = $this->processExprNode($stmt, new MethodCall($expr->var, $expr->name, $expr->args, array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true])), $nonNullabilityResult->getScope(), $nodeCallback, $context); @@ -3097,6 +3098,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } elseif ($expr instanceof PropertyFetch) { $scopeBeforeVar = $scope; $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index c3e9823f3f..23e9346326 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -276,7 +276,23 @@ public function testBug13232a(): void ], [ 'Unreachable statement - code above always terminates.', - 40, + 38, + ], + [ + 'Unreachable statement - code above always terminates.', + 44, + ], + [ + 'Unreachable statement - code above always terminates.', + 52, + ], + [ + 'Unreachable statement - code above always terminates.', + 61, + ], + [ + 'Unreachable statement - code above always terminates.', + 70, ], ]); } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php index 9aef70effe..607ba5d204 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232a.php @@ -28,7 +28,19 @@ public function sayHe(): void $callable = function (): never { exit(); }; - echo sprintf("Hello, %s no way", $callable); + echo sprintf("Hello, %s no way", $callable()); + echo 'this will never happen'; + } + + public function sayHe2(): void + { + $this->doFoo($this->neverReturnsMethod()); + echo 'this will never happen'; + } + + public function sayHe3(): void + { + self::doStaticFoo($this->neverReturnsMethod()); echo 'this will never happen'; } @@ -40,9 +52,31 @@ public function sayHuu(): void echo 'this will never happen'; } + public function sayClosure(): void + { + $callable = function (): never { + exit(); + }; + $callable(); + echo 'this will never happen'; + } + + public function sayIIFE(): void + { + (function (): never { + exit(); + })(); + + echo 'this will never happen'; + } + function neverReturnsMethod(): never { exit(); } + + public function doFoo() {} + + static public function doStaticFoo() {} } function neverReturns(): never { exit(); From 3b42642908f570d1c9f295b63f79fe69dd2ed38a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 17 Jul 2025 09:56:13 +0200 Subject: [PATCH 17/25] ExpressionResult isAlwaysTerminating is consistent with StatementResult --- src/Analyser/ExpressionResult.php | 2 +- src/Analyser/NodeScopeResolver.php | 33 ++++++++++++++++++------------ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 037093ed46..4b586b812b 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -24,11 +24,11 @@ final class ExpressionResult public function __construct( private MutatingScope $scope, private bool $hasYield, + private bool $isAlwaysTerminating, private array $throwPoints, private array $impurePoints, ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, - private bool $isAlwaysTerminating = false, ) { $this->truthyScopeCallback = $truthyScopeCallback; diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 9ae9227fb2..09c14e0f9a 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1915,7 +1915,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $nodeCallback($node, $scope); }, ExpressionContext::createDeep(), - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), false, )->getScope(); } elseif ($var instanceof PropertyFetch) { @@ -2475,7 +2475,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $scope = $scope->exitExpressionAssign($expr->expr); } - return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints, isAlwaysTerminating: $isAlwaysTerminating); + return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); }, true, ); @@ -2513,9 +2513,9 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp return new ExpressionResult( $result->getScope()->mergeWith($originalScope), $result->hasYield(), + $result->isAlwaysTerminating(), $result->getThrowPoints(), $result->getImpurePoints(), - isAlwaysTerminating: $result->isAlwaysTerminating(), ); } @@ -2933,6 +2933,7 @@ static function (): void { return new ExpressionResult( $scope, $exprResult->hasYield(), + false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), @@ -3135,6 +3136,7 @@ static function (): void { return new ExpressionResult( $scope, $exprResult->hasYield(), + false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), @@ -3172,6 +3174,7 @@ static function (): void { return new ExpressionResult( $processClosureResult->getScope(), false, + false, [], [], ); @@ -3180,6 +3183,7 @@ static function (): void { return new ExpressionResult( $result->getScope(), $result->hasYield(), + false, [], [], ); @@ -3276,6 +3280,7 @@ static function (): void { return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), + false, array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr), @@ -3296,6 +3301,7 @@ static function (): void { return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), + false, array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr), @@ -3494,7 +3500,7 @@ static function (): void { } } elseif ($expr instanceof List_) { // only in assign and foreach, processed elsewhere - return new ExpressionResult($scope, false, [], []); + return new ExpressionResult($scope, false, false, [], []); } elseif ($expr instanceof New_) { $parametersAcceptor = null; $constructorReflection = null; @@ -3646,7 +3652,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $nodeCallback($node, $scope); }, $context, - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), false, )->getScope(); } elseif ($expr instanceof Ternary) { @@ -3691,6 +3697,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { return new ExpressionResult( $finalScope, $ternaryCondResult->hasYield(), + false, $throwPoints, $impurePoints, static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr), @@ -4020,11 +4027,11 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { return new ExpressionResult( $scope, $hasYield, + $isAlwaysTerminating, $throwPoints, $impurePoints, static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), - $isAlwaysTerminating, ); } @@ -4741,7 +4748,7 @@ private function processArrowFunctionNode( $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope); $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); - return new ExpressionResult($scope, false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); + return new ExpressionResult($scope, false, false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); } /** @@ -5263,7 +5270,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $nodeCallback($node, $scope); }, $context, - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), true, ); $scope = $result->getScope(); @@ -5296,7 +5303,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { } } - return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints, isAlwaysTerminating: $isAlwaysTerminating); + return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } /** @@ -5855,7 +5862,7 @@ static function (): void { new GetOffsetValueTypeExpr($assignedExpr, $dimExpr), $nodeCallback, $context, - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), $enterExpressionAssign, ); $scope = $result->getScope(); @@ -5941,7 +5948,7 @@ static function (): void { } } - return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints, isAlwaysTerminating: $isAlwaysTerminating); + return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } /** @@ -6277,7 +6284,7 @@ private function enterForeach(MutatingScope $scope, MutatingScope $originalScope static function (): void { }, ExpressionContext::createDeep(), - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), true, )->getScope(); $vars = $this->getAssignedVariables($stmt->valueVar); @@ -6295,7 +6302,7 @@ static function (): void { static function (): void { }, ExpressionContext::createDeep(), - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), true, )->getScope(); $vars = array_merge($vars, $this->getAssignedVariables($stmt->keyVar)); From 564a7133f71e2e2d5834541cf665afcafaca02d5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 17 Jul 2025 10:18:49 +0200 Subject: [PATCH 18/25] Added ExpressionResultTest --- src/Analyser/NodeScopeResolver.php | 1 + .../PHPStan/Analyser/ExpressionResultTest.php | 77 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 tests/PHPStan/Analyser/ExpressionResultTest.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 09c14e0f9a..d6b436a523 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3201,6 +3201,7 @@ static function (): void { $impurePoints = [ new ImpurePoint($scope, $expr, $identifier, $identifier, true), ]; + $isAlwaysTerminating = true; if ($expr->expr !== null) { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); diff --git a/tests/PHPStan/Analyser/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php new file mode 100644 index 0000000000..c29d61dbde --- /dev/null +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -0,0 +1,77 @@ +getService('currentPhpVersionRichParser'); + + /** @var Stmt[] $stmts */ + $stmts = $parser->parseString(sprintf('expr; + + /** @var NodeScopeResolver $nodeScopeResolver */ + $nodeScopeResolver = self::getContainer()->getByType(NodeScopeResolver::class); + /** @var ScopeFactory $scopeFactory */ + $scopeFactory = self::getContainer()->getByType(ScopeFactory::class); + $scope = $scopeFactory->create(ScopeContext::create('test.php')) + ->assignVariable('string', new StringType(), new StringType(), TrinaryLogic::createYes()) + ->assignVariable('x', new IntegerType(), new IntegerType(), TrinaryLogic::createYes()) + ->assignVariable('cond', new MixedType(), new MixedType(), TrinaryLogic::createYes()) + ->assignVariable('arr', new ArrayType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType()), TrinaryLogic::createYes()); + + $result = $nodeScopeResolver->processExprNode( + $stmt, + $expr, + $scope, + static function (): void { + }, + ExpressionContext::createTopLevel(), + ); + $this->assertSame($expectedIsAlwaysTerminating, $result->isAlwaysTerminating()); + } + +} From b718211022359eff0365506dcce1d3d02ea34161 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 17 Jul 2025 14:31:39 +0200 Subject: [PATCH 19/25] Add $isAlwaysTerminating to every node-type if-branch --- src/Analyser/NodeScopeResolver.php | 49 ++++++++++++++++++- .../PHPStan/Analyser/ExpressionResultTest.php | 40 +++++++++++++++ tests/PHPStan/Rules/Api/composer.json | 3 ++ 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/Api/composer.json diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d6b436a523..e02e14bd07 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2415,13 +2415,13 @@ public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scop return $this->processExprNode($stmt, $newExpr, $scope, $nodeCallback, $context); } - $isAlwaysTerminating = false; $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $context); if ($expr instanceof Variable) { $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; if ($expr->name instanceof Expr) { return $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); } elseif (in_array($expr->name, Scope::SUPERGLOBAL_VARIABLES, true)) { @@ -2539,6 +2539,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $functionReflection = null; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; if ($expr->name instanceof Expr) { $nameType = $scope->getType($expr->name); if (!$nameType->isCallable()->no()) { @@ -2943,6 +2944,7 @@ static function (): void { $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; if ($expr->class instanceof Expr) { $objectClasses = $scope->getType($expr->class)->getObjectClassNames(); if (count($objectClasses) !== 1) { @@ -3106,6 +3108,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = false; $scope = $result->getScope(); if ($expr->name instanceof Expr) { $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); @@ -3154,6 +3157,7 @@ static function (): void { true, ), ]; + $isAlwaysTerminating = false; if ($expr->class instanceof Expr) { $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); @@ -3192,6 +3196,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = false; $scope = $result->getScope(); } elseif ($expr instanceof Exit_) { $hasYield = false; @@ -3213,6 +3218,7 @@ static function (): void { $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; foreach ($expr->parts as $part) { if (!$part instanceof Expr) { continue; @@ -3228,6 +3234,7 @@ static function (): void { $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; if ($expr->dim !== null) { $result = $this->processExprNode($stmt, $expr->dim, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); @@ -3246,6 +3253,7 @@ static function (): void { $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; foreach ($expr->items as $arrayItem) { $itemNodes[] = new LiteralArrayItem($scope, $arrayItem); $nodeCallback($arrayItem, $scope); @@ -3327,6 +3335,7 @@ static function (): void { $hasYield = $condResult->hasYield() || $rightResult->hasYield(); $throwPoints = array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()); $impurePoints = array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints()); + $isAlwaysTerminating = false; } elseif ($expr instanceof BinaryOp) { $result = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); @@ -3359,11 +3368,13 @@ static function (): void { true, ); $hasYield = $result->hasYield(); + $isAlwaysTerminating = false; $scope = $result->getScope()->afterExtractCall(); } elseif ($expr instanceof Expr\Print_) { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $impurePoints[] = new ImpurePoint($scope, $expr, 'print', 'print', true); $hasYield = $result->hasYield(); @@ -3372,6 +3383,7 @@ static function (): void { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = true; $hasYield = $result->hasYield(); $exprType = $scope->getType($expr->expr); @@ -3399,6 +3411,7 @@ static function (): void { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $hasYield = $result->hasYield(); $scope = $result->getScope(); @@ -3409,6 +3422,7 @@ static function (): void { $impurePoints = $result->getImpurePoints(); $impurePoints[] = new ImpurePoint($scope, $expr, 'eval', 'eval', true); $hasYield = $result->hasYield(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } elseif ($expr instanceof Expr\YieldFrom) { @@ -3424,6 +3438,7 @@ static function (): void { true, ); $hasYield = true; + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } elseif ($expr instanceof BooleanNot) { @@ -3432,7 +3447,10 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); } elseif ($expr instanceof Expr\ClassConstFetch) { + $isAlwaysTerminating = false; + if ($expr->class instanceof Expr) { $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); @@ -3463,6 +3481,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->expr); } elseif ($expr instanceof Expr\Isset_) { @@ -3470,6 +3489,7 @@ static function (): void { $throwPoints = []; $impurePoints = []; $nonNullabilityResults = []; + $isAlwaysTerminating = false; foreach ($expr->vars as $var) { $nonNullabilityResult = $this->ensureNonNullability($scope, $var); $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $var); @@ -3478,6 +3498,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $nonNullabilityResults[] = $nonNullabilityResult; } foreach (array_reverse($expr->vars) as $var) { @@ -3492,6 +3513,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); if ($expr->class instanceof Expr) { $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); @@ -3508,6 +3530,7 @@ static function (): void { $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; $className = null; if ($expr->class instanceof Expr || $expr->class instanceof Name) { if ($expr->class instanceof Expr) { @@ -3632,6 +3655,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $newExpr = $expr; if ($expr instanceof Expr\PostInc) { @@ -3660,6 +3684,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $ternaryCondResult = $this->processExprNode($stmt, $expr->cond, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $ternaryCondResult->getThrowPoints(); $impurePoints = $ternaryCondResult->getImpurePoints(); + $isAlwaysTerminating = $ternaryCondResult->isAlwaysTerminating(); $ifTrueScope = $ternaryCondResult->getTruthyScope(); $ifFalseScope = $ternaryCondResult->getFalseyScope(); $ifTrueType = null; @@ -3667,6 +3692,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $ifResult = $this->processExprNode($stmt, $expr->if, $ifTrueScope, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $ifResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $ifResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $ifResult->isAlwaysTerminating(); $ifTrueScope = $ifResult->getScope(); $ifTrueType = $ifTrueScope->getType($expr->if); } @@ -3674,6 +3700,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $elseResult = $this->processExprNode($stmt, $expr->else, $ifFalseScope, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $elseResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $elseResult->isAlwaysTerminating(); $ifFalseScope = $elseResult->getScope(); $condType = $scope->getType($expr->cond); @@ -3698,7 +3725,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { return new ExpressionResult( $finalScope, $ternaryCondResult->hasYield(), - false, + $isAlwaysTerminating, $throwPoints, $impurePoints, static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr), @@ -3718,17 +3745,20 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { true, ), ]; + $isAlwaysTerminating = false; if ($expr->key !== null) { $keyResult = $this->processExprNode($stmt, $expr->key, $scope, $nodeCallback, $context->enterDeep()); $scope = $keyResult->getScope(); $throwPoints = $keyResult->getThrowPoints(); $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $keyResult->isAlwaysTerminating(); } if ($expr->value !== null) { $valueResult = $this->processExprNode($stmt, $expr->value, $scope, $nodeCallback, $context->enterDeep()); $scope = $valueResult->getScope(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); } $hasYield = true; } elseif ($expr instanceof Expr\Match_) { @@ -3739,6 +3769,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); $impurePoints = $condResult->getImpurePoints(); + $isAlwaysTerminating = $condResult->isAlwaysTerminating(); $matchScope = $scope->enterMatch($expr); $armNodes = []; $hasDefaultCond = false; @@ -3950,23 +3981,27 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } elseif ($expr instanceof Expr\Throw_) { $hasYield = false; $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $throwPoints[] = ThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false); } elseif ($expr instanceof FunctionCallableNode) { $throwPoints = []; $impurePoints = []; $hasYield = false; + $isAlwaysTerminating = false; if ($expr->getName() instanceof Expr) { $result = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); } } elseif ($expr instanceof MethodCallableNode) { $result = $this->processExprNode($stmt, $expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); @@ -3974,23 +4009,27 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = false; if ($expr->getName() instanceof Expr) { $nameResult = $this->processExprNode($stmt, $expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $nameResult->getScope(); $hasYield = $hasYield || $nameResult->hasYield(); $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints()); + $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); } } elseif ($expr instanceof StaticMethodCallableNode) { $throwPoints = []; $impurePoints = []; $hasYield = false; + $isAlwaysTerminating = false; if ($expr->getClass() instanceof Expr) { $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $classResult->getScope(); $hasYield = $classResult->hasYield(); $throwPoints = $classResult->getThrowPoints(); $impurePoints = $classResult->getImpurePoints(); + $isAlwaysTerminating = $isAlwaysTerminating || $classResult->isAlwaysTerminating(); } if ($expr->getName() instanceof Expr) { $nameResult = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); @@ -3998,31 +4037,37 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $hasYield = $hasYield || $nameResult->hasYield(); $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $nameResult->isAlwaysTerminating(); } } elseif ($expr instanceof InstantiationCallableNode) { $throwPoints = []; $impurePoints = []; $hasYield = false; + $isAlwaysTerminating = false; if ($expr->getClass() instanceof Expr) { $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $classResult->getScope(); $hasYield = $classResult->hasYield(); $throwPoints = $classResult->getThrowPoints(); $impurePoints = $classResult->getImpurePoints(); + $isAlwaysTerminating = $isAlwaysTerminating || $classResult->isAlwaysTerminating(); } } elseif ($expr instanceof Node\Scalar) { $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; } elseif ($expr instanceof ConstFetch) { $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; $nodeCallback($expr->name, $scope); } else { $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; } return new ExpressionResult( diff --git a/tests/PHPStan/Analyser/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php index c29d61dbde..83d147df6a 100644 --- a/tests/PHPStan/Analyser/ExpressionResultTest.php +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -26,10 +26,50 @@ public static function dataIsAlwaysTerminating(): array 'sprintf("hello %s", "abc");', false, ], + [ + 'isset($x);', + false, + ], + [ + '$x ? "def" : "abc";', + false, + ], [ 'sprintf("hello %s", exit());', true, ], + [ + '(string) exit();', + true, + ], + [ + '!exit();', + true, + ], + [ + 'eval(exit());', + true, + ], + [ + 'empty(exit());', + true, + ], + [ + 'isset(exit());', + true, + ], + [ + '$x ? "abc" : exit();', + true, + ], + [ + '$x ? exit() : "abc";', + true, + ], + [ + 'fn() => yield (exit());', + true, + ], ]; } diff --git a/tests/PHPStan/Rules/Api/composer.json b/tests/PHPStan/Rules/Api/composer.json new file mode 100644 index 0000000000..7d618490ad --- /dev/null +++ b/tests/PHPStan/Rules/Api/composer.json @@ -0,0 +1,3 @@ +{ + "name": "phpstan/foo" +} \ No newline at end of file From 6daf17c476597b5a5dc33dcd7ea0cb40c209010e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 17 Jul 2025 14:37:04 +0200 Subject: [PATCH 20/25] fix build --- src/Analyser/NodeScopeResolver.php | 8 ++++---- tests/PHPStan/Analyser/ExpressionResultTest.php | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index e02e14bd07..054002c42f 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3383,7 +3383,7 @@ static function (): void { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); - $isAlwaysTerminating = true; + $isAlwaysTerminating = $result->isAlwaysTerminating(); $hasYield = $result->hasYield(); $exprType = $scope->getType($expr->expr); @@ -3751,7 +3751,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $scope = $keyResult->getScope(); $throwPoints = $keyResult->getThrowPoints(); $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); - $isAlwaysTerminating = $isAlwaysTerminating || $keyResult->isAlwaysTerminating(); + $isAlwaysTerminating = $keyResult->isAlwaysTerminating(); } if ($expr->value !== null) { $valueResult = $this->processExprNode($stmt, $expr->value, $scope, $nodeCallback, $context->enterDeep()); @@ -4029,7 +4029,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $hasYield = $classResult->hasYield(); $throwPoints = $classResult->getThrowPoints(); $impurePoints = $classResult->getImpurePoints(); - $isAlwaysTerminating = $isAlwaysTerminating || $classResult->isAlwaysTerminating(); + $isAlwaysTerminating = $classResult->isAlwaysTerminating(); } if ($expr->getName() instanceof Expr) { $nameResult = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); @@ -4050,7 +4050,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $hasYield = $classResult->hasYield(); $throwPoints = $classResult->getThrowPoints(); $impurePoints = $classResult->getImpurePoints(); - $isAlwaysTerminating = $isAlwaysTerminating || $classResult->isAlwaysTerminating(); + $isAlwaysTerminating = $classResult->isAlwaysTerminating(); } } elseif ($expr instanceof Node\Scalar) { $hasYield = false; diff --git a/tests/PHPStan/Analyser/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php index 83d147df6a..8b09915be4 100644 --- a/tests/PHPStan/Analyser/ExpressionResultTest.php +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -34,6 +34,10 @@ public static function dataIsAlwaysTerminating(): array '$x ? "def" : "abc";', false, ], + [ + '(string) $x;', + false, + ], [ 'sprintf("hello %s", exit());', true, From 3de67089941412fb6b553a94042058b7d300018d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 17 Jul 2025 17:02:11 +0200 Subject: [PATCH 21/25] fix arrow-fn and suppress --- src/Analyser/NodeScopeResolver.php | 6 +++--- tests/PHPStan/Analyser/ExpressionResultTest.php | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 054002c42f..d77a225de7 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3187,7 +3187,7 @@ static function (): void { return new ExpressionResult( $result->getScope(), $result->hasYield(), - false, + $result->isAlwaysTerminating(), [], [], ); @@ -3196,7 +3196,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); - $isAlwaysTerminating = false; + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } elseif ($expr instanceof Exit_) { $hasYield = false; @@ -4794,7 +4794,7 @@ private function processArrowFunctionNode( $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope); $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); - return new ExpressionResult($scope, false, false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); + return new ExpressionResult($scope, false, $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); } /** diff --git a/tests/PHPStan/Analyser/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php index 8b09915be4..9160412ff1 100644 --- a/tests/PHPStan/Analyser/ExpressionResultTest.php +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -74,6 +74,10 @@ public static function dataIsAlwaysTerminating(): array 'fn() => yield (exit());', true, ], + [ + '@exit();', + true, + ], ]; } From a3616009a97c80af00d84a8e073e5a61a6d19f9e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 17 Jul 2025 17:13:55 +0200 Subject: [PATCH 22/25] implement more cases --- src/Analyser/NodeScopeResolver.php | 8 +++---- .../PHPStan/Analyser/ExpressionResultTest.php | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d77a225de7..9ad02eab8e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3289,7 +3289,7 @@ static function (): void { return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), - false, + $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr), @@ -3310,7 +3310,7 @@ static function (): void { return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), - false, + $leftResult->isAlwaysTerminating(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr), @@ -3335,7 +3335,7 @@ static function (): void { $hasYield = $condResult->hasYield() || $rightResult->hasYield(); $throwPoints = array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()); $impurePoints = array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints()); - $isAlwaysTerminating = false; + $isAlwaysTerminating = $condResult->isAlwaysTerminating(); } elseif ($expr instanceof BinaryOp) { $result = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); @@ -3368,7 +3368,7 @@ static function (): void { true, ); $hasYield = $result->hasYield(); - $isAlwaysTerminating = false; + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope()->afterExtractCall(); } elseif ($expr instanceof Expr\Print_) { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); diff --git a/tests/PHPStan/Analyser/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php index 9160412ff1..f35481396b 100644 --- a/tests/PHPStan/Analyser/ExpressionResultTest.php +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -38,6 +38,14 @@ public static function dataIsAlwaysTerminating(): array '(string) $x;', false, ], + [ + '$x || exit();', + false, + ], + [ + '$x ?? exit();', + false, + ], [ 'sprintf("hello %s", exit());', true, @@ -78,6 +86,22 @@ public static function dataIsAlwaysTerminating(): array '@exit();', true, ], + [ + '$x && exit();', + true, + ], + [ + 'exit() && $x;', + true, + ], + [ + 'exit() || $x;', + true, + ], + [ + 'exit() ?? $x;', + true, + ], ]; } From 26f888b7c09b92825dd011c0719854d2dfc689a8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 17 Jul 2025 17:46:18 +0200 Subject: [PATCH 23/25] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 9ad02eab8e..c22dfc3b54 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2934,7 +2934,7 @@ static function (): void { return new ExpressionResult( $scope, $exprResult->hasYield(), - false, + $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), @@ -3115,6 +3115,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); if ($this->phpVersion->supportsPropertyHooks()) { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); @@ -3139,7 +3140,7 @@ static function (): void { return new ExpressionResult( $scope, $exprResult->hasYield(), - false, + $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), @@ -3163,6 +3164,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } if ($expr->name instanceof Expr) { @@ -3170,6 +3172,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); } } elseif ($expr instanceof Expr\Closure) { @@ -3240,6 +3243,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } @@ -3247,6 +3251,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); } elseif ($expr instanceof Array_) { $itemNodes = []; @@ -3457,6 +3462,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); } else { $hasYield = false; $throwPoints = []; @@ -3470,6 +3476,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } else { $nodeCallback($expr->name, $scope); } @@ -3520,6 +3527,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } } elseif ($expr instanceof List_) { // only in assign and foreach, processed elsewhere @@ -3549,6 +3557,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); foreach ($additionalThrowPoints as $throwPoint) { $throwPoints[] = $throwPoint; } @@ -3644,6 +3653,7 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } elseif ( $expr instanceof Expr\PreInc || $expr instanceof Expr\PostInc From 180c665bd1a5779916a2062d2b52672aefa10969 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 17 Jul 2025 17:47:51 +0200 Subject: [PATCH 24/25] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index c22dfc3b54..b997339e9c 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2555,6 +2555,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $scope = $nameResult->getScope(); $throwPoints = $nameResult->getThrowPoints(); $impurePoints = $nameResult->getImpurePoints(); + $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); if ( $nameType->isObject()->yes() && $nameType->isCallable()->yes() @@ -2961,6 +2962,7 @@ static function (): void { $hasYield = $classResult->hasYield(); $throwPoints = array_merge($throwPoints, $classResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $classResult->getImpurePoints()); + $isAlwaysTerminating = $classResult->isAlwaysTerminating(); foreach ($additionalThrowPoints as $throwPoint) { $throwPoints[] = $throwPoint; } @@ -3108,14 +3110,14 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); - $isAlwaysTerminating = false; + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); if ($expr->name instanceof Expr) { $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); - $isAlwaysTerminating = $result->isAlwaysTerminating(); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); if ($this->phpVersion->supportsPropertyHooks()) { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); From 5fb5b06d818adb29cee6a25f24aa5bb579b6914a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 17 Jul 2025 17:57:33 +0200 Subject: [PATCH 25/25] fix nullsafe --- src/Analyser/NodeScopeResolver.php | 4 ++-- tests/PHPStan/Rules/Api/composer.json | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 tests/PHPStan/Rules/Api/composer.json diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index b997339e9c..9da212aa64 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2935,7 +2935,7 @@ static function (): void { return new ExpressionResult( $scope, $exprResult->hasYield(), - $exprResult->isAlwaysTerminating(), + false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), @@ -3142,7 +3142,7 @@ static function (): void { return new ExpressionResult( $scope, $exprResult->hasYield(), - $exprResult->isAlwaysTerminating(), + false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), diff --git a/tests/PHPStan/Rules/Api/composer.json b/tests/PHPStan/Rules/Api/composer.json deleted file mode 100644 index 7d618490ad..0000000000 --- a/tests/PHPStan/Rules/Api/composer.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "phpstan/foo" -} \ No newline at end of file