psalm icon indicating copy to clipboard operation
psalm copied to clipboard

templated types considered always different

Open greg0ire opened this issue 2 years ago • 1 comments

I don't understand why Psalm thinks the type of $element cannot possibly be the same as the templated type in the wrapped collection in this code snippet. IMO it is a bug.

https://psalm.dev/r/9c680774e8

greg0ire avatar Sep 29 '22 06:09 greg0ire

I found these snippets:

https://psalm.dev/r/9c680774e8
<?php

/**
 * @template-covariant T
 */
interface ReadableCollection
{
    
    /**
     * @psalm-param TMaybeContained $element
     *
     * @psalm-return (TMaybeContained is T ? bool : false)
     *
     * @template TMaybeContained
     */
    public function contains(mixed $element): bool;
}

/**
 * @psalm-template T
 * @template-implements ReadableCollection<T>
 */
class PersistentCollection implements ReadableCollection
{
    /**
     * @psalm-var ReadableCollection<T>
     */
    private ReadableCollection $collection;
    
    /**
     * @psalm-param ReadableCollection<T> $collection The collection elements.
     */
    public function __construct(ReadableCollection $collection)
    {
        $this->collection  = $collection;
    }

    /**
     * {@inheritDoc}
     */
    public function contains(mixed $element): bool
    {
        return $this->collection->contains($element) || rand(0, 2) > 1;
    }
}
Psalm output (using commit 028ac7f):

ERROR: DocblockTypeContradiction - 43:16 - Operand of type false is always falsy

psalm-github-bot[bot] avatar Sep 29 '22 06:09 psalm-github-bot[bot]

We have found the same today. We were upgrading to Doctrine/collections 1.8 and found that the following PR moved some inheritance paths for some methods (https://github.com/doctrine/collections/pull/322) but it was unclear why this broke our extension of Doctrine\ArrayCollection

We tested this in the psalm sandbox with a minimum case and found the same issue https://psalm.dev/r/f830dbce24. We could fix this if, instead of using inheritdoc, we just copied the docblocks https://psalm.dev/r/69ba5ea045

M1ke avatar Nov 15 '22 13:11 M1ke

I found these snippets:

https://psalm.dev/r/f830dbce24
<?php

/**
 * @psalm-template TKey of array-key
 * @template-covariant T
 */
interface ReadableCollection
{
    /**
     * Gets the element at the specified key/index.
     *
     * @param string|int $key The key/index of the element to retrieve.
     * @psalm-param TKey $key
     *
     * @return mixed
     * @psalm-return T|null
     */
    public function get($key);
}

/**
 * @psalm-template TKey of array-key
 * @psalm-template T
 * @template-extends ReadableCollection<TKey, T>
 * @template-extends ArrayAccess<TKey, T>
 */
interface Collection extends ReadableCollection
{
  /**
     * Sets an element in the collection at the specified key/index.
     *
     * @param string|int $key   The key/index of the element to set.
     * @param mixed      $value The element to set.
     * @psalm-param TKey $key
     * @psalm-param T $value
     *
     * @return void
     */
    public function set($key, $value);
}

/**
 * @psalm-template TKey of array-key
 * @psalm-template T
 * @template-implements Collection<TKey,T>
 * @psalm-consistent-constructor
 */
class ArrayCollection implements Collection
{
    /**
     * An array containing the entries of this collection.
     *
     * @psalm-var array<TKey,T>
     * @var mixed[]
     */
    private $elements;
    
    /**
     * Initializes a new ArrayCollection.
     *
     * @param array $elements
     * @psalm-param array<TKey,T> $elements
     */
    public function __construct(array $elements = [])
    {
        $this->elements = $elements;
    }
    
    /**
     * {@inheritDoc}
     */
    public function get($key)
    {
        return $this->elements[$key] ?? null;
    }
    
    /**
     * {@inheritDoc}
     */
    public function set($key, $value)
    {
        $this->elements[$key] = $value;
    }
}


/**
 * @template TKey of array-key
 * @template T
 * @extends ArrayCollection<TKey, T>
 */
class Foo extends ArrayCollection {
}

class Bar {}

$bars = [new Bar, new Bar];
$bar_collection = new Foo($bars);

$bar = $bar_collection->get(1);
if ($bar)
{
    $bar_collection->set(0, $bar);
}
Psalm output (using commit 2a29fd7):

INFO: MixedArgumentTypeCoercion - 103:29 - Argument 2 of Foo::set expects Bar, but parent type T:Foo as mixed provided
https://psalm.dev/r/69ba5ea045
<?php

/**
 * @psalm-template TKey of array-key
 * @template-covariant T
 */
interface ReadableCollection
{
    /**
     * Gets the element at the specified key/index.
     *
     * @param string|int $key The key/index of the element to retrieve.
     * @psalm-param TKey $key
     *
     * @return mixed
     * @psalm-return T|null
     */
    public function get($key);
}

/**
 * @psalm-template TKey of array-key
 * @psalm-template T
 * @template-extends ReadableCollection<TKey, T>
 * @template-extends ArrayAccess<TKey, T>
 */
interface Collection extends ReadableCollection
{
  /**
     * Sets an element in the collection at the specified key/index.
     *
     * @param string|int $key   The key/index of the element to set.
     * @param mixed      $value The element to set.
     * @psalm-param TKey $key
     * @psalm-param T $value
     *
     * @return void
     */
    public function set($key, $value);
}

/**
 * @psalm-template TKey of array-key
 * @psalm-template T
 * @template-implements Collection<TKey,T>
 * @psalm-consistent-constructor
 */
class ArrayCollection implements Collection
{
    /**
     * An array containing the entries of this collection.
     *
     * @psalm-var array<TKey,T>
     * @var mixed[]
     */
    private $elements;
    
    /**
     * Initializes a new ArrayCollection.
     *
     * @param array $elements
     * @psalm-param array<TKey,T> $elements
     */
    public function __construct(array $elements = [])
    {
        $this->elements = $elements;
    }
    
    /**
     * Gets the element at the specified key/index.
     *
     * @param string|int $key The key/index of the element to retrieve.
     * @psalm-param TKey $key
     *
     * @return mixed
     * @psalm-return T|null
     */
    public function get($key)
    {
        return $this->elements[$key] ?? null;
    }
    
  /**
     * Sets an element in the collection at the specified key/index.
     *
     * @param string|int $key   The key/index of the element to set.
     * @param mixed      $value The element to set.
     * @psalm-param TKey $key
     * @psalm-param T $value
     *
     * @return void
     */
    public function set($key, $value)
    {
        $this->elements[$key] = $value;
    }
}


/**
 * @template TKey of array-key
 * @template T
 * @extends ArrayCollection<TKey, T>
 */
class Foo extends ArrayCollection {
}

class Bar {}

$bars = [new Bar, new Bar];
$bar_collection = new Foo($bars);

$bar = $bar_collection->get(1);
if ($bar)
{
    $bar_collection->set(0, $bar);
}
Psalm output (using commit 2a29fd7):

No issues!

psalm-github-bot[bot] avatar Nov 15 '22 13:11 psalm-github-bot[bot]