psalm icon indicating copy to clipboard operation
psalm copied to clipboard

Union type in phpdoc passes and gets an error at the same time

Open pkly opened this issue 1 year ago • 1 comments

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.

pkly avatar Sep 23 '24 11:09 pkly

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

psalm-github-bot[bot] avatar Sep 23 '24 11:09 psalm-github-bot[bot]