psalm icon indicating copy to clipboard operation
psalm copied to clipboard

False positive InvalidArgument when using template

Open mj4444ru opened this issue 3 months ago • 5 comments

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

mj4444ru avatar Sep 19 '25 12:09 mj4444ru

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!

psalm-github-bot[bot] avatar Sep 19 '25 12:09 psalm-github-bot[bot]

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.

mj4444ru avatar Sep 19 '25 13:09 mj4444ru

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

psalm-github-bot[bot] avatar Sep 19 '25 13:09 psalm-github-bot[bot]

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

pkly avatar Oct 20 '25 09:10 pkly

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

psalm-github-bot[bot] avatar Oct 20 '25 09:10 psalm-github-bot[bot]