False positive InvalidArgument when using template
I don't understand what's wrong here. Is it a psalm error, or am I doing something wrong?
https://psalm.dev/r/31f4dc9a12
Removing the template annotations eliminates the error.
https://psalm.dev/r/2230d85b9a
I found these snippets:
https://psalm.dev/r/31f4dc9a12
<?php
/**
* @template TResponse of ResponseInterface
*/
interface RequestInterface {}
/**
* @template TRequest of RequestInterface
*/
interface ResponseInterface {}
/**
* @template TResponse of BaseResponse
* @implements RequestInterface<TResponse>
*/
abstract class BaseRequest implements RequestInterface {}
/**
* @template TRequest of BaseRequest
* @implements ResponseInterface<TRequest>
*/
abstract class BaseResponse implements ResponseInterface {
public function test1(): void
{
$this->f($this);
testGlobal($this);
}
protected function f(ResponseInterface $response): void {}
}
/**
* @extends BaseRequest<JsonResponse>
*/
final class JsonRequest extends BaseRequest {}
/**
* @extends BaseResponse<JsonRequest>
*/
final class JsonResponse extends BaseResponse
{
public function test2(): void
{
$this->f($this);
testGlobal($this);
}
}
/** @psalm-suppress UnusedParam */
function testGlobal(ResponseInterface $response): void {}
Psalm output (using commit cdceda0):
ERROR: InvalidArgument - 45:18 - Argument 1 of JsonResponse::f expects ResponseInterface<RequestInterface<ResponseInterface>>, but JsonResponse provided
ERROR: InvalidArgument - 46:20 - Argument 1 of testGlobal expects ResponseInterface<RequestInterface>, but JsonResponse provided
https://psalm.dev/r/2230d85b9a
<?php
interface RequestInterface {}
interface ResponseInterface {}
abstract class BaseRequest implements RequestInterface {}
abstract class BaseResponse implements ResponseInterface {
public function test1(): void
{
$this->f($this);
testGlobal($this);
}
protected function f(ResponseInterface $response): void {}
}
final class JsonRequest extends BaseRequest {}
final class JsonResponse extends BaseResponse
{
public function test2(): void
{
$this->f($this);
testGlobal($this);
}
}
/** @psalm-suppress UnusedParam */
function testGlobal(ResponseInterface $response): void {}
Psalm output (using commit cdceda0):
No issues!
A very simplified example.
https://psalm.dev/r/8406c51c9d
Since there's no error in the test1 function in the first example, I don't expect one in the test2 function.
I found these snippets:
https://psalm.dev/r/8406c51c9d
<?php
interface RequestInterface {}
final class Request implements RequestInterface {}
/**
* @template TRequest of RequestInterface
*/
interface ResponseInterface {}
/**
* @implements ResponseInterface<Request>
*/
abstract class BaseResponse implements ResponseInterface {}
final class Response extends BaseResponse
{
public function test2(): void
{
$this->f($this);
testGlobal($this);
}
protected function f(ResponseInterface $response): void {}
}
/** @psalm-suppress UnusedParam */
function testGlobal(ResponseInterface $response): void {}
Psalm output (using commit cdceda0):
ERROR: InvalidArgument - 18:18 - Argument 1 of Response::f expects ResponseInterface<RequestInterface>, but Response provided
ERROR: InvalidArgument - 19:20 - Argument 1 of testGlobal expects ResponseInterface<RequestInterface>, but Response provided
I see something similar here with occurrences in our live code
https://psalm.dev/r/5df20b8b8f
PHPStan does not raise any errors at level 7 and I'm pretty sure this is correct
I found these snippets:
https://psalm.dev/r/5df20b8b8f
<?php
/**
* @template T
*/
interface Foo {}
/**
* @implements Foo<int>
*/
class Bar implements Foo {}
#[\Attribute]
class FooAttribute {
/**
* @param class-string<Foo<mixed>> $var
*/
public function __construct(public string $var) {}
}
#[FooAttribute(Bar::class)]
class Collector {
/**
* @var array<int, Foo<mixed>>
*/
private array $instances = [];
}
Psalm output (using commit cdceda0):
ERROR: InvalidArgument - 21:16 - Argument 1 of FooAttribute::__construct expects class-string<Foo<mixed>>, but Bar::class provided