psalm
psalm copied to clipboard
templated types considered always different
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
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
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
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!