psalm
psalm copied to clipboard
Union type in phpdoc passes and gets an error at the same time
After upgrading from 5.24 I saw a new issue in known-good code, related to incorrect callback arguments when using symfony cacke, as psalm seems to get slightly confused. I think I've managed to reproduce the minimal issue with the following snippet, but in case you're curious the full(er) snippet is also available below.
https://psalm.dev/r/8276870767
// The Symfony Cache definition
namespace Symfony\Contracts\Cache;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\InvalidArgumentException;
interface CacheInterface
{
/**
* Fetches a value from the pool or computes it if not found.
*
* On cache misses, a callback is called that should return the missing value.
* This callback is given a PSR-6 CacheItemInterface instance corresponding to the
* requested key, that could be used e.g. for expiration control. It could also
* be an ItemInterface instance when its additional features are needed.
*
* @template T
*
* @param string $key The key of the item to retrieve from the cache
* @param (callable(CacheItemInterface,bool):T)|(callable(ItemInterface,bool):T)|CallbackInterface<T> $callback
* @param float|null $beta A float that, as it grows, controls the likeliness of triggering
* early expiration. 0 disables it, INF forces immediate expiration.
* The default (or providing null) is implementation dependent but should
* typically be 1.0, which should provide optimal stampede protection.
* See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration
* @param array &$metadata The metadata of the cached item {@see ItemInterface::getMetadata()}
*
* @return T
*
* @throws InvalidArgumentException When $key is not valid or when $beta is negative
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null): mixed;
}
our code
return $this->cache->get(
'some-cache-name',
function (Symfony\Contracts\Cache\ItemInterface $item) use ($foo) {
$item->expiresAfter(60 * 60 * 24)
->tag($foo->getCacheTags());
// do stuff and return
}
);
Psalm errors out here with:
ERROR: MismatchingDocblockParamType - file.php:21:23 - Parameter $item has wrong type 'Psr\Cache\CacheItemInterface', should be 'Symfony\Contracts\Cache\ItemInterface' (see https://psalm.dev/141)
function (ItemInterface $item) use ($foo) {
ERROR: UndefinedInterfaceMethod - file.php:23:23 - Method Psr\Cache\CacheItemInterface::tag does not exist (see https://psalm.dev/181)
->tag($foo->getCacheTags());
It appears psalm gets confused and compares the definition to both declarations, instead of simply selecting the "best" one and bailing out.
I found these snippets:
https://psalm.dev/r/8276870767
<?php
class Foo {}
class Bar {}
/**
* @template T
*
* @param (callable(Foo, bool):T)|(callable(Bar, bool):T) $callback
*/
function something(callable $callback): void {
$callback(random_int(0, 1) ? new Foo() : new Bar(), true);
}
Psalm output (using commit 16b24bd):
ERROR: PossiblyInvalidArgument - 13:15 - Argument 1 expects Bar, but possibly different type Bar|Foo provided