diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index 70e28b85895..b7eb0e843e5 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -13,6 +13,7 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; @@ -22,6 +23,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use function array_merge; use function get_class; use function sprintf; @@ -35,6 +37,7 @@ final class AssignOpHandler implements ExprHandler public function __construct( private AssignHandler $assignHandler, private InitializerExprTypeResolver $initializerExprTypeResolver, + private ImplicitToStringCallHelper $implicitToStringCallHelper, ) { } @@ -85,19 +88,25 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex } $scope = $assignResult->getScope(); $throwPoints = $assignResult->getThrowPoints(); + $impurePoints = $assignResult->getImpurePoints(); if ( ($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) && !$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() ) { $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false); } + if ($expr instanceof Expr\AssignOp\Concat) { + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope); + $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); + } return new ExpressionResult( $scope, hasYield: $assignResult->hasYield(), isAlwaysTerminating: $assignResult->isAlwaysTerminating(), throwPoints: $throwPoints, - impurePoints: $assignResult->getImpurePoints(), + impurePoints: $impurePoints, truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index b9d0908f78f..6f9fbbb71cb 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -13,6 +13,7 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; @@ -42,6 +43,7 @@ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, private PhpVersion $phpVersion, + private ImplicitToStringCallHelper $implicitToStringCallHelper, ) { } @@ -62,12 +64,19 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $leftResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep()); $rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftResult->getScope(), $storage, $nodeCallback, $context->enterDeep()); $throwPoints = array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()); + $impurePoints = array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()); if ( ($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) && !$leftResult->getScope()->getType($expr->right)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() ) { $throwPoints[] = InternalThrowPoint::createExplicit($leftResult->getScope(), new ObjectType(DivisionByZeroError::class), $expr, false); } + if ($expr instanceof BinaryOp\Concat) { + $leftToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->left, $scope); + $rightToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->right, $leftResult->getScope()); + $throwPoints = array_merge($throwPoints, $leftToStringResult->getThrowPoints(), $rightToStringResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $leftToStringResult->getImpurePoints(), $rightToStringResult->getImpurePoints()); + } $scope = $rightResult->getScope(); return new ExpressionResult( @@ -75,7 +84,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(), throwPoints: $throwPoints, - impurePoints: array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), + impurePoints: $impurePoints, truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index 336e14dd921..b4e119e01f5 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -4,21 +4,18 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\Cast; -use PhpParser\Node\Identifier; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; -use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; -use PHPStan\Analyser\ImpurePoint; +use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Type; -use function sprintf; +use function array_merge; /** * @implements ExprHandler @@ -29,8 +26,7 @@ final class CastStringHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, - private PhpVersion $phpVersion, - private MethodThrowPointHelper $methodThrowPointHelper, + private ImplicitToStringCallHelper $implicitToStringCallHelper, ) { } @@ -46,31 +42,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = $exprResult->getImpurePoints(); $throwPoints = $exprResult->getThrowPoints(); - $exprType = $scope->getType($expr->expr); - $toStringMethod = $scope->getMethodReflection($exprType, '__toString'); - if ($toStringMethod !== null) { - if (!$toStringMethod->hasSideEffects()->no()) { - $impurePoints[] = new ImpurePoint( - $scope, - $expr, - 'methodCall', - sprintf('call to method %s::%s()', $toStringMethod->getDeclaringClass()->getDisplayName(), $toStringMethod->getName()), - $toStringMethod->isPure()->no(), - ); - } - - if ($this->phpVersion->throwsOnStringCast()) { - $throwPoint = $this->methodThrowPointHelper->getThrowPoint( - $toStringMethod, - $toStringMethod->getOnlyVariant(), - new Expr\MethodCall($expr->expr, new Identifier('__toString')), - $scope, - ); - if ($throwPoint !== null) { - $throwPoints[] = $throwPoint; - } - } - } + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope); + $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); $scope = $exprResult->getScope(); diff --git a/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php new file mode 100644 index 00000000000..1b66bc7ba38 --- /dev/null +++ b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php @@ -0,0 +1,73 @@ +getType($expr); + $toStringMethod = $scope->getMethodReflection($exprType, '__toString'); + if ($toStringMethod === null) { + return new ExpressionResult( + $scope, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + ); + } + + if (!$toStringMethod->hasSideEffects()->no()) { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'methodCall', + sprintf('call to method %s::%s()', $toStringMethod->getDeclaringClass()->getDisplayName(), $toStringMethod->getName()), + $toStringMethod->isPure()->no(), + ); + } + + if ($this->phpVersion->throwsOnStringCast()) { + $throwPoint = $this->methodThrowPointHelper->getThrowPoint( + $toStringMethod, + $toStringMethod->getOnlyVariant(), + new Expr\MethodCall($expr, new Identifier('__toString')), + $scope, + ); + if ($throwPoint !== null) { + $throwPoints[] = $throwPoint; + } + } + + return new ExpressionResult( + $scope, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + ); + } + +} diff --git a/src/Analyser/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/ExprHandler/InterpolatedStringHandler.php index 24e4dcab0ec..bb18cff3e09 100644 --- a/src/Analyser/ExprHandler/InterpolatedStringHandler.php +++ b/src/Analyser/ExprHandler/InterpolatedStringHandler.php @@ -10,6 +10,7 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; @@ -27,6 +28,7 @@ final class InterpolatedStringHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ImplicitToStringCallHelper $implicitToStringCallHelper, ) { } @@ -50,6 +52,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $hasYield = $hasYield || $partResult->hasYield(); $throwPoints = array_merge($throwPoints, $partResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $partResult->getImpurePoints()); + + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($part, $scope); + $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $partResult->isAlwaysTerminating(); $scope = $partResult->getScope(); } diff --git a/src/Analyser/ExprHandler/PrintHandler.php b/src/Analyser/ExprHandler/PrintHandler.php index 71f0e2d8c13..18ab04d8d29 100644 --- a/src/Analyser/ExprHandler/PrintHandler.php +++ b/src/Analyser/ExprHandler/PrintHandler.php @@ -9,6 +9,7 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; @@ -24,6 +25,12 @@ final class PrintHandler implements ExprHandler { + public function __construct( + private ImplicitToStringCallHelper $implicitToStringCallHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Print_; @@ -37,14 +44,21 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); + $throwPoints = $exprResult->getThrowPoints(); + $impurePoints = $exprResult->getImpurePoints(); + + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope); + $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); + $scope = $exprResult->getScope(); return new ExpressionResult( $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), - throwPoints: $exprResult->getThrowPoints(), - impurePoints: array_merge($exprResult->getImpurePoints(), [new ImpurePoint($scope, $expr, 'print', 'print', true)]), + throwPoints: $throwPoints, + impurePoints: array_merge($impurePoints, [new ImpurePoint($scope, $expr, 'print', 'print', true)]), ); } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 001c8b5b368..f616b644ab3 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -49,6 +49,7 @@ use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; use PHPStan\Analyser\ExprHandler\AssignHandler; +use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\BetterReflection\Reflection\ReflectionEnum; use PHPStan\BetterReflection\Reflector\Reflector; @@ -236,6 +237,7 @@ public function __construct( private readonly bool $implicitThrows, #[AutowiredParameter] private readonly bool $treatPhpDocTypesAsCertain, + private readonly ImplicitToStringCallHelper $implicitToStringCallHelper, ) { $earlyTerminatingMethodNames = []; @@ -861,19 +863,22 @@ public function processStmtNode( } elseif ($stmt instanceof Echo_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; $isAlwaysTerminating = false; foreach ($stmt->exprs as $echoExpr) { $result = $this->processExprNode($stmt, $echoExpr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($echoExpr, $scope); + $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } $throwPoints = $overridingThrowPoints ?? $throwPoints; - $impurePoints = [ - new ImpurePoint($scope, $stmt, 'echo', 'echo', true), - ]; + $impurePoints[] = new ImpurePoint($scope, $stmt, 'echo', 'echo', true); return new InternalStatementResult($scope, $hasYield, $isAlwaysTerminating, [], $throwPoints, $impurePoints); } elseif ($stmt instanceof Return_) { if ($stmt->expr !== null) { diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 0565407424e..d8d0ba20e1c 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Analyser; use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Error; +use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\Fiber\FiberNodeScopeResolver; use PHPStan\Analyser\FileAnalyser; use PHPStan\Analyser\IgnoreErrorExtensionProvider; @@ -116,6 +117,7 @@ protected function createNodeScopeResolver(): NodeScopeResolver [], self::getContainer()->getParameter('exceptions')['implicitThrows'], $this->shouldTreatPhpDocTypesAsCertain(), + self::getContainer()->getByType(ImplicitToStringCallHelper::class), ); } diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index fb793c45c91..7723560fd06 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; +use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\Fiber\FiberNodeScopeResolver; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; @@ -91,6 +92,7 @@ protected static function createNodeScopeResolver(): NodeScopeResolver static::getEarlyTerminatingFunctionCalls(), $container->getParameter('exceptions')['implicitThrows'], $container->getParameter('treatPhpDocTypesAsCertain'), + $container->getByType(ImplicitToStringCallHelper::class), ); } diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 27f9daddb07..39da6a5be9e 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -6,6 +6,7 @@ use PhpParser\Lexer; use PhpParser\NodeVisitor\NameResolver; use PhpParser\Parser\Php7; +use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\Ignore\IgnoredErrorHelper; use PHPStan\Analyser\Ignore\IgnoreLexer; use PHPStan\Collectors\Registry as CollectorRegistry; @@ -832,6 +833,7 @@ private function createAnalyser(): Analyser [], true, $this->shouldTreatPhpDocTypesAsCertain(), + $container->getByType(ImplicitToStringCallHelper::class), ); $lexer = new Lexer(); $fileAnalyser = new FileAnalyser( diff --git a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php index e963de6bc98..85ee19ddd05 100644 --- a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php +++ b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser\Fiber; use PhpParser\Node; +use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; @@ -136,6 +137,7 @@ protected function createNodeScopeResolver(): NodeScopeResolver [], self::getContainer()->getParameter('exceptions')['implicitThrows'], $this->shouldTreatPhpDocTypesAsCertain(), + self::getContainer()->getByType(ImplicitToStringCallHelper::class), ); } diff --git a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php index 5bea8cce792..952730be751 100644 --- a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser\Fiber; +use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; @@ -69,6 +70,7 @@ protected static function createNodeScopeResolver(): NodeScopeResolver static::getEarlyTerminatingFunctionCalls(), $container->getParameter('exceptions')['implicitThrows'], $container->getParameter('treatPhpDocTypesAsCertain'), + $container->getByType(ImplicitToStringCallHelper::class), ); } diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 03e5554b19c..86615a292dc 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -748,4 +748,42 @@ public function testBug13806(): void ]); } + public function testBug5952(): void + { + $this->analyse([__DIR__ . '/data/bug-5952.php'], [ + [ + 'Dead catch - Exception is never thrown in the try block.', + 51, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 57, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 84, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 97, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 110, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 125, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 138, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 151, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-5952.php b/tests/PHPStan/Rules/Exceptions/data/bug-5952.php new file mode 100644 index 00000000000..9c8cc81a15c --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-5952.php @@ -0,0 +1,153 @@ += 7.4 + +namespace Bug5952; + +class Foo +{ + public function __toString(): string + { + throw new \Exception(); + } +} + +$foo = new Foo(); + +try { + echo $foo; +} catch (\Exception $e) { + echo "Should be printed"; +} + +class Bar +{ + /** @throws \Exception */ + public function __toString(): string + { + throw new \Exception(); + } +} + +$bar = new Bar(); + +try { + echo $bar; +} catch (\Exception $e) { + echo "Should be printed"; +} + +class Baz +{ + /** @throws void */ + public function __toString(): string + { + return 'hello'; + } +} + +$baz = new Baz(); + +try { + echo $baz; +} catch (\Exception $e) { + echo "Should not be printed"; +} + +try { + echo 123; +} catch (\Exception $e) { + echo "Should not be printed"; +} + +/** @var int|Foo $intOrFoo */ +$intOrFoo = doFoo(); + +try { + echo $intOrFoo; +} catch (\Exception $e) { + echo "Should be printed"; +} + +/** @var int|Bar $intOrBar */ +$intOrBar = doFoo(); + +try { + echo $intOrBar; +} catch (\Exception $e) { + echo "Should be printed"; +} + +/** @var int|Baz $intOrBaz */ +$intOrBaz = doFoo(); + +try { + echo $intOrBaz; +} catch (\Exception $e) { + echo "Should not be printed"; +} + +// print statement +try { + print $foo; +} catch (\Exception $e) { + echo "Should be printed"; +} + +try { + print $baz; +} catch (\Exception $e) { + echo "Should not be printed"; +} + +// String concatenation +try { + $x = 'hello' . $foo; +} catch (\Exception $e) { + echo "Should be printed"; +} + +try { + $x = 'hello' . $baz; +} catch (\Exception $e) { + echo "Should not be printed"; +} + +// Concat assignment +try { + $x = 'hello'; + $x .= $foo; +} catch (\Exception $e) { + echo "Should be printed"; +} + +try { + $x = 'hello'; + $x .= $baz; +} catch (\Exception $e) { + echo "Should not be printed"; +} + +// String interpolation +try { + $x = "hello $foo"; +} catch (\Exception $e) { + echo "Should be printed"; +} + +try { + $x = "hello $baz"; +} catch (\Exception $e) { + echo "Should not be printed"; +} + +// String interpolation with curly braces +try { + $x = "hello {$foo}"; +} catch (\Exception $e) { + echo "Should be printed"; +} + +try { + $x = "hello {$baz}"; +} catch (\Exception $e) { + echo "Should not be printed"; +} diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php index 9c1a95adc9f..aea4a376c12 100644 --- a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -133,21 +133,89 @@ public function testRule(): void 'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doFoo().', 296, ], + [ + 'Impure echo in pure method PureMethod\TestMagicMethods::doEcho().', + 309, + ], + [ + 'Impure echo in pure method PureMethod\TestMagicMethods::doEcho().', + 310, + ], + [ + 'Possibly impure call to method PureMethod\MaybePureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doEcho().', + 311, + ], + [ + 'Impure echo in pure method PureMethod\TestMagicMethods::doEcho().', + 311, + ], + [ + 'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doEcho().', + 312, + ], + [ + 'Impure echo in pure method PureMethod\TestMagicMethods::doEcho().', + 312, + ], + [ + 'Impure print in pure method PureMethod\TestMagicMethods::doPrint().', + 324, + ], + [ + 'Possibly impure call to method PureMethod\MaybePureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doPrint().', + 325, + ], + [ + 'Impure print in pure method PureMethod\TestMagicMethods::doPrint().', + 325, + ], + [ + 'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doPrint().', + 326, + ], + [ + 'Impure print in pure method PureMethod\TestMagicMethods::doPrint().', + 326, + ], + [ + 'Possibly impure call to method PureMethod\MaybePureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doConcat().', + 339, + ], + [ + 'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doConcat().', + 340, + ], + [ + 'Possibly impure call to method PureMethod\MaybePureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doConcatAssign().', + 355, + ], + [ + 'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doConcatAssign().', + 357, + ], + [ + 'Possibly impure call to method PureMethod\MaybePureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doInterpolation().', + 370, + ], + [ + 'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doInterpolation().', + 371, + ], [ 'Possibly impure call to a callable in pure method PureMethod\MaybeCallableFromUnion::doFoo().', - 330, + 405, ], [ 'Possibly impure call to a callable in pure method PureMethod\MaybeCallableFromUnion::doFoo().', - 330, + 405, ], [ 'Impure static property access in pure method PureMethod\StaticMethodAccessingStaticProperty::getA().', - 388, + 463, ], [ 'Impure property assignment in pure method PureMethod\StaticMethodAssigningStaticProperty::getA().', - 409, + 484, ], ]); } diff --git a/tests/PHPStan/Rules/Pure/data/pure-method.php b/tests/PHPStan/Rules/Pure/data/pure-method.php index eca2976cda5..761afcd73b5 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-method.php +++ b/tests/PHPStan/Rules/Pure/data/pure-method.php @@ -296,6 +296,81 @@ public function doFoo( (string) $impure; } + /** + * @phpstan-pure + */ + public function doEcho( + NoMagicMethods $no, + PureMagicMethods $pure, + MaybePureMagicMethods $maybe, + ImpureMagicMethods $impure + ) + { + echo $no; + echo $pure; + echo $maybe; + echo $impure; + } + + /** + * @phpstan-pure + */ + public function doPrint( + PureMagicMethods $pure, + MaybePureMagicMethods $maybe, + ImpureMagicMethods $impure + ) + { + print $pure; + print $maybe; + print $impure; + } + + /** + * @phpstan-pure + */ + public function doConcat( + PureMagicMethods $pure, + MaybePureMagicMethods $maybe, + ImpureMagicMethods $impure + ) + { + 'hello' . $pure; + 'hello' . $maybe; + 'hello' . $impure; + } + + /** + * @phpstan-pure + */ + public function doConcatAssign( + PureMagicMethods $pure, + MaybePureMagicMethods $maybe, + ImpureMagicMethods $impure + ) + { + $x = 'hello'; + $x .= $pure; + $x = 'hello'; + $x .= $maybe; + $x = 'hello'; + $x .= $impure; + } + + /** + * @phpstan-pure + */ + public function doInterpolation( + PureMagicMethods $pure, + MaybePureMagicMethods $maybe, + ImpureMagicMethods $impure + ) + { + "hello $pure"; + "hello $maybe"; + "hello $impure"; + } + } final class NoConstructor