diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 0a50465169..4b586b812b 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -24,6 +24,7 @@ final class ExpressionResult public function __construct( private MutatingScope $scope, private bool $hasYield, + private bool $isAlwaysTerminating, private array $throwPoints, private array $impurePoints, ?callable $truthyScopeCallback = null, @@ -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..9da212aa64 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()); @@ -926,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()); @@ -1910,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) { @@ -2416,6 +2421,7 @@ public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scop $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)) { @@ -2462,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, $isAlwaysTerminating, $throwPoints, $impurePoints); }, true, ); @@ -2476,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; @@ -2505,6 +2513,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp return new ExpressionResult( $result->getScope()->mergeWith($originalScope), $result->hasYield(), + $result->isAlwaysTerminating(), $result->getThrowPoints(), $result->getImpurePoints(), ); @@ -2518,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() @@ -2529,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()) { @@ -2544,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() @@ -2559,6 +2571,7 @@ static function (): void { ); $throwPoints = array_merge($throwPoints, $invokeResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $invokeResult->getImpurePoints()); + $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) { @@ -2593,12 +2606,15 @@ static function (): void { if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; + $returnType = $parametersAcceptor->getReturnType(); + $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); @@ -2803,6 +2819,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); @@ -2849,6 +2866,8 @@ static function (): void { if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; + $returnType = $parametersAcceptor->getReturnType(); + $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); } $result = $this->processArgs( @@ -2907,6 +2926,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); @@ -2915,6 +2935,7 @@ static function (): void { return new ExpressionResult( $scope, $exprResult->hasYield(), + false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), @@ -2924,6 +2945,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) { @@ -2940,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; } @@ -3035,6 +3058,8 @@ static function (): void { if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; + $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(); @@ -3078,18 +3103,21 @@ 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()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $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 = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); if ($this->phpVersion->supportsPropertyHooks()) { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); @@ -3114,6 +3142,7 @@ static function (): void { return new ExpressionResult( $scope, $exprResult->hasYield(), + false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), @@ -3131,11 +3160,13 @@ static function (): void { true, ), ]; + $isAlwaysTerminating = false; if ($expr->class instanceof Expr) { $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } if ($expr->name instanceof Expr) { @@ -3143,6 +3174,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) { @@ -3151,6 +3183,7 @@ static function (): void { return new ExpressionResult( $processClosureResult->getScope(), false, + false, [], [], ); @@ -3159,6 +3192,7 @@ static function (): void { return new ExpressionResult( $result->getScope(), $result->hasYield(), + $result->isAlwaysTerminating(), [], [], ); @@ -3167,6 +3201,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } elseif ($expr instanceof Exit_) { $hasYield = false; @@ -3176,6 +3211,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(); @@ -3187,6 +3223,7 @@ static function (): void { $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; foreach ($expr->parts as $part) { if (!$part instanceof Expr) { continue; @@ -3195,17 +3232,20 @@ 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) { $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; if ($expr->dim !== null) { $result = $this->processExprNode($stmt, $expr->dim, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } @@ -3213,12 +3253,14 @@ 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 = []; $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; foreach ($expr->items as $arrayItem) { $itemNodes[] = new LiteralArrayItem($scope, $arrayItem); $nodeCallback($arrayItem, $scope); @@ -3227,6 +3269,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(); } @@ -3234,6 +3277,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); @@ -3252,6 +3296,7 @@ static function (): void { return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), + $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr), @@ -3272,6 +3317,7 @@ static function (): void { return new ExpressionResult( $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), + $leftResult->isAlwaysTerminating(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr), @@ -3296,12 +3342,14 @@ static function (): void { $hasYield = $condResult->hasYield() || $rightResult->hasYield(); $throwPoints = array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()); $impurePoints = array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints()); + $isAlwaysTerminating = $condResult->isAlwaysTerminating(); } elseif ($expr instanceof BinaryOp) { $result = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $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 +3361,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(); @@ -3326,11 +3375,13 @@ static function (): void { true, ); $hasYield = $result->hasYield(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $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(); @@ -3339,6 +3390,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(); $exprType = $scope->getType($expr->expr); @@ -3366,6 +3418,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(); @@ -3376,6 +3429,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) { @@ -3391,6 +3445,7 @@ static function (): void { true, ); $hasYield = true; + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); } elseif ($expr instanceof BooleanNot) { @@ -3399,13 +3454,17 @@ 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(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); } else { $hasYield = false; $throwPoints = []; @@ -3419,6 +3478,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); } @@ -3430,6 +3490,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_) { @@ -3437,6 +3498,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); @@ -3445,6 +3507,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) { @@ -3459,22 +3522,25 @@ 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(); $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 - return new ExpressionResult($scope, false, [], []); + return new ExpressionResult($scope, false, false, [], []); } elseif ($expr instanceof New_) { $parametersAcceptor = null; $constructorReflection = null; $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; $className = null; if ($expr->class instanceof Expr || $expr->class instanceof Name) { if ($expr->class instanceof Expr) { @@ -3493,6 +3559,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); foreach ($additionalThrowPoints as $throwPoint) { $throwPoints[] = $throwPoint; } @@ -3588,6 +3655,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 @@ -3599,6 +3667,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $newExpr = $expr; if ($expr instanceof Expr\PostInc) { @@ -3620,13 +3689,14 @@ 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) { $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; @@ -3634,6 +3704,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); } @@ -3641,6 +3712,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); @@ -3665,6 +3737,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { return new ExpressionResult( $finalScope, $ternaryCondResult->hasYield(), + $isAlwaysTerminating, $throwPoints, $impurePoints, static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr), @@ -3684,17 +3757,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 = $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_) { @@ -3705,6 +3781,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; @@ -3916,23 +3993,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()); @@ -3940,23 +4021,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 = $classResult->isAlwaysTerminating(); } if ($expr->getName() instanceof Expr) { $nameResult = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); @@ -3964,36 +4049,43 @@ 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 = $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( $scope, $hasYield, + $isAlwaysTerminating, $throwPoints, $impurePoints, static fn (): MutatingScope => $scope->filterByTruthyValue($expr), @@ -4714,7 +4806,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, $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); } /** @@ -4992,6 +5084,7 @@ private function processArgs( $hasYield = false; $throwPoints = []; $impurePoints = []; + $isAlwaysTerminating = false; foreach ($args as $i => $arg) { $assignByReference = false; $parameter = null; @@ -5141,6 +5234,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(); @@ -5155,6 +5249,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()); } } } @@ -5232,7 +5328,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(); @@ -5265,7 +5361,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { } } - return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); + return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } /** @@ -5372,12 +5468,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); } @@ -5462,6 +5560,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); @@ -5513,6 +5612,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); @@ -5618,6 +5718,7 @@ static function (): void { $hasYield = $objectResult->hasYield(); $throwPoints = $objectResult->getThrowPoints(); $impurePoints = $objectResult->getImpurePoints(); + $isAlwaysTerminating = $objectResult->isAlwaysTerminating(); $scope = $objectResult->getScope(); $propertyName = null; @@ -5628,6 +5729,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(); } @@ -5636,6 +5738,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()) { @@ -5723,6 +5826,7 @@ static function (): void { $hasYield = $propertyNameResult->hasYield(); $throwPoints = $propertyNameResult->getThrowPoints(); $impurePoints = $propertyNameResult->getImpurePoints(); + $isAlwaysTerminating = $propertyNameResult->isAlwaysTerminating(); $scope = $propertyNameResult->getScope(); } @@ -5731,6 +5835,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) { @@ -5775,6 +5880,7 @@ static function (): void { $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); foreach ($var->items as $i => $arrayItem) { if ($arrayItem === null) { @@ -5792,6 +5898,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(); } @@ -5799,6 +5906,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); @@ -5812,13 +5920,14 @@ 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(); $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 = []; @@ -5897,7 +6006,7 @@ static function (): void { } } - return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); + return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } /** @@ -6233,7 +6342,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); @@ -6251,7 +6360,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)); diff --git a/tests/PHPStan/Analyser/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php new file mode 100644 index 0000000000..f35481396b --- /dev/null +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -0,0 +1,149 @@ + yield (exit());', + true, + ], + [ + '@exit();', + true, + ], + [ + '$x && exit();', + true, + ], + [ + 'exit() && $x;', + true, + ], + [ + 'exit() || $x;', + true, + ], + [ + 'exit() ?? $x;', + true, + ], + ]; + } + + #[DataProvider('dataIsAlwaysTerminating')] + public function testIsAlwaysTerminating( + string $code, + bool $expectedIsAlwaysTerminating, + ): void + { + /** @var Parser $parser */ + $parser = self::getContainer()->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()); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 87e0dd0c11..23e9346326 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,4 +241,100 @@ 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 + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232a.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 10, + ], + [ + 'Unreachable statement - code above always terminates.', + 17, + ], + [ + 'Unreachable statement - code above always terminates.', + 23, + ], + [ + 'Unreachable statement - code above always terminates.', + 32, + ], + [ + 'Unreachable statement - code above always terminates.', + 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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug13232b(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232b.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 19, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + 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, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + 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-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 @@ +neverReturnsMethod()); + echo 'this will never happen'; + } + + public function sayHi(): void + { + echo 'Hello, ' . neverReturns() + . ' no way'; + echo 'this will never happen'; + } + + public function sayHo(): void + { + echo "Hello, {$this->neverReturnsMethod()} no way"; + 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'; + } + + 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'; + } + + public function sayHuu(): void + { + $x = [ + $this->neverReturnsMethod() + ]; + 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(); +} + diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13232b.php b/tests/PHPStan/Rules/DeadCode/data/bug-13232b.php new file mode 100644 index 0000000000..9818fb7849 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13232b.php @@ -0,0 +1,34 @@ += 8.0 + +namespace Bug13232c; + +final class HelloWorld +{ + public function sayHello(): void + { + echo 'Hello, ' . $this->returnNever() + . ' no way'; + + echo 'this will never happen'; + } + + static public function sayStaticHello(): void + { + echo 'Hello, ' . self::staticReturnNever() + . ' no way'; + + echo 'this will never happen'; + } + + public function sayNullsafeHello(?self $x): void + { + echo 'Hello, ' . $x?->returnNever() + . ' no way'; + + echo 'this might happen, in case $x is null'; + } + + public function sayMaybeHello(): void + { + if (rand(0, 1)) { + echo 'Hello, ' . $this->returnNever() + . ' no way'; + } + + echo 'this might happen'; + } + + function returnNever(): never + + { + exit(); + } + + static function staticReturnNever(): never + { + exit(); + } + +} 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 @@ +