Skip to content
Merged
20 changes: 19 additions & 1 deletion src/Analyser/ExprHandler/CastStringHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +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\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\Type\Type;
use function sprintf;
Expand All @@ -26,6 +29,8 @@ final class CastStringHandler implements ExprHandler

public function __construct(
private InitializerExprTypeResolver $initializerExprTypeResolver,
private PhpVersion $phpVersion,
private MethodThrowPointHelper $methodThrowPointHelper,
)
{
}
Expand All @@ -39,6 +44,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
{
$exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
$impurePoints = $exprResult->getImpurePoints();
$throwPoints = $exprResult->getThrowPoints();

$exprType = $scope->getType($expr->expr);
$toStringMethod = $scope->getMethodReflection($exprType, '__toString');
Expand All @@ -52,6 +58,18 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$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;
}
}
}

$scope = $exprResult->getScope();
Expand All @@ -60,7 +78,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$scope,
hasYield: $exprResult->hasYield(),
isAlwaysTerminating: $exprResult->isAlwaysTerminating(),
throwPoints: $exprResult->getThrowPoints(),
throwPoints: $throwPoints,
impurePoints: $impurePoints,
truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
Expand Down
98 changes: 98 additions & 0 deletions src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser\ExprHandler\Helper;

use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\InternalThrowPoint;
use PHPStan\Analyser\MutatingScope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use ReflectionFunction;
use ReflectionMethod;
use Throwable;
use function in_array;

#[AutowiredService]
final class MethodThrowPointHelper
{

public function __construct(
private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
private bool $implicitThrows,
)
{
}

public function getThrowPoint(
MethodReflection $methodReflection,
ParametersAcceptor $parametersAcceptor,
MethodCall|StaticCall $normalizedMethodCall,
MutatingScope $scope,
): ?InternalThrowPoint
{
if ($normalizedMethodCall instanceof MethodCall) {
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) {
if (!$extension->isMethodSupported($methodReflection)) {
continue;
}

$throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $normalizedMethodCall, $scope);
if ($throwType === null) {
return null;
}

return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, false);
}
} else {
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) {
if (!$extension->isStaticMethodSupported($methodReflection)) {
continue;
}

$throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $normalizedMethodCall, $scope);
if ($throwType === null) {
return null;
}

return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, false);
}
}

if (
$normalizedMethodCall instanceof MethodCall
&& in_array($methodReflection->getName(), ['invoke', 'invokeArgs'], true)
&& in_array($methodReflection->getDeclaringClass()->getName(), [ReflectionMethod::class, ReflectionFunction::class], true)
) {
return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall);
}

$throwType = $methodReflection->getThrowType();
if ($throwType === null) {
$returnType = $parametersAcceptor->getReturnType();
if ($returnType instanceof NeverType && $returnType->isExplicit()) {
$throwType = new ObjectType(Throwable::class);
}
}

if ($throwType !== null) {
if (!$throwType->isVoid()->yes()) {

Check warning on line 85 in src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } if ($throwType !== null) { - if (!$throwType->isVoid()->yes()) { + if ($throwType->isVoid()->no()) { return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, true); } } elseif ($this->implicitThrows) {

Check warning on line 85 in src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } if ($throwType !== null) { - if (!$throwType->isVoid()->yes()) { + if ($throwType->isVoid()->no()) { return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, true); } } elseif ($this->implicitThrows) {
return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, true);
}
} elseif ($this->implicitThrows) {
$methodReturnedType = $scope->getType($normalizedMethodCall);
if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {

Check warning on line 90 in src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } } elseif ($this->implicitThrows) { $methodReturnedType = $scope->getType($normalizedMethodCall); - if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + if ((new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->no()) { return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall); } }

Check warning on line 90 in src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ } } elseif ($this->implicitThrows) { $methodReturnedType = $scope->getType($normalizedMethodCall); - if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + if (!$methodReturnedType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall); } }

Check warning on line 90 in src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } } elseif ($this->implicitThrows) { $methodReturnedType = $scope->getType($normalizedMethodCall); - if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + if ((new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->no()) { return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall); } }

Check warning on line 90 in src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ } } elseif ($this->implicitThrows) { $methodReturnedType = $scope->getType($normalizedMethodCall); - if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + if (!$methodReturnedType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall); } }
return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall);
}
}

return null;
}

}
59 changes: 3 additions & 56 deletions src/Analyser/ExprHandler/MethodCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,31 @@
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper;
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper;
use PHPStan\Analyser\ImpurePoint;
use PHPStan\Analyser\InternalThrowPoint;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
use PHPStan\Node\Expr\PossiblyImpureCallExpr;
use PHPStan\Node\InvalidateExprNode;
use PHPStan\Reflection\Callables\SimpleImpurePoint;
use PHPStan\Reflection\ExtendedParametersAcceptor;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use ReflectionFunction;
use ReflectionMethod;
use Throwable;
use function array_map;
use function array_merge;
use function count;
use function in_array;
use function sprintf;
use function strtolower;

Expand All @@ -57,10 +50,8 @@ final class MethodCallHandler implements ExprHandler
{

public function __construct(
private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
private MethodCallReturnTypeHelper $methodCallReturnTypeHelper,
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
private bool $implicitThrows,
private MethodThrowPointHelper $methodThrowPointHelper,
#[AutowiredParameter]
private bool $rememberPossiblyImpureFunctionValues,
)
Expand Down Expand Up @@ -153,7 +144,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$scope = $argsResult->getScope();

if ($methodReflection !== null) {
$methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope);
$methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope);
if ($methodThrowPoint !== null) {
$throwPoints[] = $methodThrowPoint;
}
Expand Down Expand Up @@ -235,50 +226,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
return $result;
}

private function getMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, MethodCall $normalizedMethodCall, MutatingScope $scope): ?InternalThrowPoint
{
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) {
if (!$extension->isMethodSupported($methodReflection)) {
continue;
}

$throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $normalizedMethodCall, $scope);
if ($throwType === null) {
return null;
}

return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, false);
}

if (
in_array($methodReflection->getName(), ['invoke', 'invokeArgs'], true)
&& in_array($methodReflection->getDeclaringClass()->getName(), [ReflectionMethod::class, ReflectionFunction::class], true)
) {
return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall);
}

$throwType = $methodReflection->getThrowType();
if ($throwType === null) {
$returnType = $parametersAcceptor->getReturnType();
if ($returnType instanceof NeverType && $returnType->isExplicit()) {
$throwType = new ObjectType(Throwable::class);
}
}

if ($throwType !== null) {
if (!$throwType->isVoid()->yes()) {
return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, true);
}
} elseif ($this->implicitThrows) {
$methodReturnedType = $scope->getType($normalizedMethodCall);
if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall);
}
}

return null;
}

public function resolveType(MutatingScope $scope, Expr $expr): Type
{
if ($expr->name instanceof Identifier) {
Expand Down
39 changes: 3 additions & 36 deletions src/Analyser/ExprHandler/StaticCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper;
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper;
use PHPStan\Analyser\ImpurePoint;
use PHPStan\Analyser\InternalThrowPoint;
Expand All @@ -25,7 +26,6 @@
use PHPStan\Analyser\NoopNodeCallback;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
use PHPStan\Node\Expr\PossiblyImpureCallExpr;
use PHPStan\Reflection\Callables\SimpleImpurePoint;
use PHPStan\Reflection\MethodReflection;
Expand All @@ -39,7 +39,6 @@
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeWithClassName;
use ReflectionProperty;
use Throwable;
use function array_map;
use function array_merge;
use function count;
Expand All @@ -55,10 +54,8 @@ final class StaticCallHandler implements ExprHandler
{

public function __construct(
private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
private MethodCallReturnTypeHelper $methodCallReturnTypeHelper,
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
private bool $implicitThrows,
private MethodThrowPointHelper $methodThrowPointHelper,
#[AutowiredParameter]
private bool $rememberPossiblyImpureFunctionValues,
)
Expand Down Expand Up @@ -198,7 +195,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$scopeFunction = $scope->getFunction();

if ($methodReflection !== null) {
$methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $normalizedExpr, $scope);
$methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope);
if ($methodThrowPoint !== null) {
$throwPoints[] = $methodThrowPoint;
}
Expand Down Expand Up @@ -268,36 +265,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
);
}

private function getStaticMethodThrowPoint(MethodReflection $methodReflection, StaticCall $normalizedMethodCall, MutatingScope $scope): ?InternalThrowPoint
{
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) {
if (!$extension->isStaticMethodSupported($methodReflection)) {
continue;
}

$throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $normalizedMethodCall, $scope);
if ($throwType === null) {
return null;
}

return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, false);
}

if ($methodReflection->getThrowType() !== null) {
$throwType = $methodReflection->getThrowType();
if (!$throwType->isVoid()->yes()) {
return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, true);
}
} elseif ($this->implicitThrows) {
$methodReturnedType = $scope->getType($normalizedMethodCall);
if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall);
}
}

return null;
}

public function resolveType(MutatingScope $scope, Expr $expr): Type
{
if ($expr->name instanceof Identifier) {
Expand Down
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -506,4 +506,9 @@ public function hasFilterThrowOnFailureConstant(): bool
return $this->versionId >= 80500;
}

public function throwsOnStringCast(): bool
{
return $this->versionId >= 70400;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,24 @@ public function testPropertyHooks(): void
]);
}

public function testBug13806(): void
{
$this->analyse([__DIR__ . '/data/bug-13806.php'], [
[
'Dead catch - Throwable is never thrown in the try block.',
8,
],
[
'Dead catch - Throwable is never thrown in the try block.',
53,
],
[
'Dead catch - Throwable is never thrown in the try block.',
64,
],
]);
}

public static function getAdditionalConfigFiles(): array
{
return array_merge(
Expand Down
Loading
Loading