New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Wrong dead catch analysis when using immediately called callback #4819
Comments
I ran into the very same issue, subscribing |
@ondrejmirtes Do you have any input towards this issue? This is a blocker for us to upgrade to version >= 0.12.83, which I would love to do. |
PHPStan bug is rarely a blocker for upgrading to a newer version. In most cases you can easily ignore any false positive. I thought about this and I think there are two ways to solve this:
|
Yeah, I can ignore all the invalid dead catches + all the "always-true" cases etc, but this affects plenty of places in our codebase (since we use this approach a lot) so we would have to adjust the ignores alongside many refactorings which is not negligible effort. Dynamic throw seems like a best option to me. Adding inline throws everywhere would be crazy in our case. Thx |
Implemented by: phpstan/phpstan-src@2bf30bf Documentation: https://phpstan.org/developing-extensions/dynamic-throw-type-extensions Your extension should inspect the AST of the closure argument to see if there's any There might be an opportunity in the future to explore more precise closure analysis with annotations like |
Thanks a lot! Do you have any hint how to inspect the AST easily? Is there any way I can reuse your ThrowPoints implemented recently? Or do I really need to go through the AST deeply and check all method calls and throws? |
Alright, just tried implementing it, this works: <?php
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
class ImmediatelyCalledClosureInMethodCallThrowTypeExtension implements \PHPStan\Type\DynamicMethodThrowTypeExtension
{
private NodeScopeResolver $nodeScopeResolver;
public function __construct(NodeScopeResolver $nodeScopeResolver)
{
$this->nodeScopeResolver = $nodeScopeResolver;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getDeclaringClass()->getName() === HelloWorld::class
&& $methodReflection->getName() === 'lock';
}
public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
if (!isset($methodCall->args[0])) {
return $methodReflection->getThrowType();
}
$arg = $methodCall->args[0]->value;
if (!$arg instanceof \PhpParser\Node\Expr\Closure) {
return $methodReflection->getThrowType();
}
assert($scope instanceof MutatingScope);
$closureBodyResult = $this->nodeScopeResolver->processStmtNodes(
$methodCall,
$arg->stmts,
$scope->enterAnonymousFunction($arg),
static function (): void {}
);
$throwPoints = $closureBodyResult->getThrowPoints();
if (count($throwPoints) === 0) {
return $methodReflection->getThrowType();
}
$types = [];
if ($methodReflection->getThrowType() !== null) {
$types[] = $methodReflection->getThrowType();
}
foreach ($throwPoints as $throwPoint) {
$types[] = $throwPoint->getType();
}
return TypeCombinator::union(...$types);
}
} |
Well I managed to achieve it by using public function getThrowTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): ?Type {
$throwTypes = $methodReflection->getThrowType() !== null
? [$methodReflection->getThrowType()]
: [];
... // get deps for NodeScopeResolver call
$result = $nodeScopeResolver->processStmtNodes($closure, $closure->getStmts(), $methodScope);
foreach ($result->getThrowPoints() as $throwPoint) {
$throwTypes[] = $throwPoint->getType();
}
return TypeCombinator::union(... $throwTypes);
} Any hint for better implementation will be appreciated, thank you |
Just posted it, looks similar :D |
Great, you were faster, seems like the complicated deps for NodeScopeResolver are gone in your solution, thanks! |
@ondrejmirtes Are you considering also the inline throws annotation? After fixing the rest of codebase, I found few cases where it would help me not ignoring the dead catch. /**
* @throws MyEntityAlreadyExistsException
*/
private function flushChanges(): void
{
try {
$this->entityManager->flush();
} catch (UniqueConstraintViolationException $e) {
if (Strings::endsWith($e->getMessage(), sprintf("'%s'", MyEntity::UNIQUE_INDEX_NAME))) {
throw new MyEntityAlreadyExistsException();
}
throw $e;
}
} Now this piece of code produces those errors I need to ignore:
|
@janedbal Yes, but in this case it would be better to add |
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Bug report
Code snippet that reproduces the problem
Expected output
The text was updated successfully, but these errors were encountered: