vimeo / psalm

A static analysis tool for finding errors in PHP applications
https://psalm.dev
MIT License
5.56k stars 660 forks source link

Using @psalm-assert to assert a value is a Closure doesn't satisfy a Closure return type #10280

Open asgrim opened 1 year ago

asgrim commented 1 year ago

Got a confusing one, might just be my understanding, or a weird edge case bug.

When using an assertion function (such as \Webmozart\Assert::assertInstanceOf) to assert a value is a Closure appears to correctly do an assertion, but Psalm reports that the inferred type Closure does not match the declared return type Closure, which is a confusing error message for sure!

ERROR: [InvalidReturnStatement](https://psalm.dev/128) - 36:12 - The inferred type 'Closure' does not match the declared return type 'Closure' for makesClosure

Example of the error: https://psalm.dev/r/f041c54218

Not sure if it helps, but the issue can be side-stepped by doing an if/throw, for example in: https://psalm.dev/r/365931c953

psalm-github-bot[bot] commented 1 year ago

I found these snippets:

https://psalm.dev/r/f041c54218 ```php $class * @psalm-assert ExpectedType $value * * @param mixed $value * @param string|object $class * * @throws InvalidArgumentException */ function assertInstanceOf($value, $class): void { if (!($value instanceof $class)) { throw new InvalidArgumentException('lol'); } } function makesClosure(ContainerInterface $c): Closure { /** @var mixed */ $thing = $c->get('anything'); assertInstanceOf($thing, Closure::class); /** @psalm-trace $thing */ return $thing; } ``` ``` Psalm output (using commit 9b00ac0): ERROR: InvalidReturnStatement - 36:12 - The inferred type 'Closure' does not match the declared return type 'Closure' for makesClosure INFO: Trace - 36:5 - $thing: Closure ERROR: InvalidReturnType - 30:47 - The declared return type 'Closure' for makesClosure is incorrect, got 'Closure' ```
https://psalm.dev/r/365931c953 ```php get('some-closure-from-the-container'); if (! $thing instanceof Closure) { throw new \InvalidArgumentException('was not a closure'); } /** @psalm-trace $thing */ return $thing; } ``` ``` Psalm output (using commit 9b00ac0): INFO: Trace - 20:5 - $thing: Closure ```
asgrim commented 1 year ago

Oh forgot to say - this only appears to happen with Closure - if I replace it with any other standard defined class, the same assertion works: https://psalm.dev/r/a153bf3546

psalm-github-bot[bot] commented 1 year ago

I found these snippets:

https://psalm.dev/r/a153bf3546 ```php $class * @psalm-assert ExpectedType $value * * @param mixed $value * @param string|object $class * * @throws InvalidArgumentException */ function assertInstanceOf($value, $class): void { if (!($value instanceof $class)) { throw new InvalidArgumentException('lol'); } } class X {} function makesX(ContainerInterface $c): X { /** @var mixed */ $thing = $c->get('anything'); assertInstanceOf($thing, X::class); /** @psalm-trace $thing */ return $thing; } ``` ``` Psalm output (using commit 9b00ac0): INFO: Trace - 38:5 - $thing: X ```