Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

False-positive InvalidTemplateParam with @template-covariant T when a callable parameter accepts T #10848

Open
Shira-3749 opened this issue Mar 21, 2024 · 3 comments

Comments

@Shira-3749
Copy link

When a class defines @template-covariant T and one of the methods has a callable parameter which accepts T as an argument, I'm getting an InvalidTemplateParam error:

https://psalm.dev/r/2eb66f01fe

I'm not sure the error is valid in this case. (And it seems to work fine otherwise.)

PHPStan doesn't seem to mind this: https://phpstan.org/r/ac9be5aa-4437-40c3-b55d-3ffaa236cb67
But will also error when it's an actual issue: https://phpstan.org/r/96d33c99-1ff7-4541-b024-c8cfaea34e30

I've also tried it in C# (as a language with native generics support) and it seems to allow this as well:

public interface IExample<out T>
{
    public void call(Action<T> callback);
}

(out is equivalent to @template-covariant here)

Copy link

I found these snippets:

https://psalm.dev/r/2eb66f01fe
<?php

/**
 * @template-covariant T
 */
class Example
{
    /**
     * @param T $value
     */
    function __construct(private mixed $value)
    {}
    
    /**
     * @param callable(T):void $callback 
     */
    function call(callable $callback): void
    {
    	$callback($this->value);
    }
}
Psalm output (using commit ef3b018):

ERROR: InvalidTemplateParam - 15:15 - Template param T of Example is marked covariant and cannot be used here
https://psalm.dev/r/3ebad94817
<?php

/**
 * @template-covariant T
 */
class Example
{
    /**
     * @param T $value
     */
    function __construct(private mixed $value)
    {}
    
    /**
     * @psalm-suppress InvalidTemplateParam
     * @param callable(T):void $callback 
     */
    function call(callable $callback): void
    {
    	$callback($this->value);
    }
}

class Animal {}
class Cat extends Animal {}
class Dog extends Animal {}

/** @psalm-suppress UnusedClosureParam */
function exampleWithObjects(): void
{
    $example = new Example(new Cat());
    
    $example->call(function (Animal $animal) {});
    $example->call(function (Cat $animal) {});
    $example->call(function (Dog $animal) {}); // OK (expected error)
}

/**
 * @param Cat[] $cats
 * @param callable(Animal[]):void $takesAnimals
 * @param callable(Cat[]):void $takesCats
 * @param callable(Dog[]):void $takesDogs
 */
function exampleWithArrays(
    array $cats,
    callable $takesAnimals,
    callable $takesCats,
    callable $takesDogs,
): void {
    $example = new Example($cats);
    
    $example->call($takesAnimals);
    $example->call($takesCats);
    $example->call($takesDogs); // OK (expected error)
}


/**
 * @param \ArrayObject<int, Cat> $cats
 * @param callable(\ArrayObject<int, Animal>):void $takesAnimals
 * @param callable(\ArrayObject<int, Cat>):void $takesCats
 * @param callable(\ArrayObject<int, Dog>):void $takesDogs
 */
function exampleWithArrayObjects(
    \ArrayObject $cats,
    callable $takesAnimals,
    callable $takesCats,
    callable $takesDogs,
): void {
    $example = new Example($cats);
    
    $example->call($takesAnimals); // OK (expected error)
    $example->call($takesCats);
    $example->call($takesDogs);  // OK (expected error)
}
Psalm output (using commit ef3b018):

ERROR: InvalidArgument - 35:20 - Argument 1 of Example::call expects callable(Cat):void, but pure-Closure(Dog):void provided

ERROR: InvalidArgument - 54:20 - Argument 1 of Example::call expects callable(array<array-key, Cat>):void, but callable(array<array-key, Dog>):void provided

ERROR: InvalidArgument - 72:20 - Argument 1 of Example::call expects callable(ArrayObject<int, Cat>):void, but callable(ArrayObject<int, Animal>):void provided

ERROR: InvalidArgument - 74:20 - Argument 1 of Example::call expects callable(ArrayObject<int, Cat>):void, but callable(ArrayObject<int, Dog>):void provided

@weirdan
Copy link
Collaborator

weirdan commented Mar 21, 2024

Note to future me: callable(T):void should be valid in this context, but callable():T shouldn't.

@vkurdin
Copy link

vkurdin commented May 18, 2024

It would be awesome if Psalm supported this Scala machinery on callables and variance: https://scastie.scala-lang.org/ZbzouFbvRHGPSwwB8OrNoA
+A stands for @template-covariant A
-A is @template-contravariant A
Unit is void
A => Boolean is callable(A): bool
(A => Boolean) => Boolean is callable(callable(A): bool): bool

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants