vimeo / psalm

A static analysis tool for finding errors in PHP applications
https://psalm.dev
MIT License
5.57k stars 660 forks source link

Add support for template default types #5407

Open simPod opened 3 years ago

simPod commented 3 years ago

If template is impossible to resolve, eg. because the type is T|null, it should be possible to specify a default type

Consider this: If a value passed for $a is null, it's impossible to resolve TResult's type. Then default type should be used. Currently, the default type everywhere is mixed instead.

/**
 * @template T
 */
interface I {
    /**
     * @template TResult
     * @param (callable(T): TResult)|null $a
     * @return I<TResult>
     */
  public function work(callable|null $a = null): I;
}

It should be possible to say that TResult default is T. Therefore, when calling work(null) on I<string>, the return type would be string.

https://psalm.dev/r/254659fcc2


This feature is already present in other languages like typescript or java

interface I<T>
{
    work<TResult = T>(a: ((value: T) => TResult) | null): TResult;
}
interface Promise<T> {
   then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
}
psalm-github-bot[bot] commented 3 years ago

I found these snippets:

https://psalm.dev/r/254659fcc2 ```php */ public function work(callable|null $a = null): I; } /** * @param I $i */ function x(I $i) : string { return $i->work(null); } ``` ``` Psalm output (using commit dd4d970): ERROR: InvalidReturnStatement - 20:9 - The inferred type 'I' does not match the declared return type 'string' for x ERROR: InvalidReturnType - 19:20 - The declared return type 'string' for x is incorrect, got 'I' ```
weirdan commented 3 years ago

This use-case is already covered by conditional types: https://psalm.dev/r/2cf8b73f59

psalm-github-bot[bot] commented 3 years ago

I found these snippets:

https://psalm.dev/r/2cf8b73f59 ```php ) */ public function work(callable|null $a = null): I; } /** * @param I $i */ function x(I $i) : string { return $i->work(null); } ``` ``` Psalm output (using commit dd4d970): No issues! ```
simPod commented 3 years ago

Ah well in that case I guess it's solved. Thanks!

simPod commented 3 years ago

This is still a bit wonky if condition gets more complicated (one more param appears).

Also, I'm either doing something wrong or it's not fully supported?

https://psalm.dev/r/57576478fc

psalm-github-bot[bot] commented 3 years ago

I found these snippets:

https://psalm.dev/r/57576478fc ```php * : I * ) * : ( * $b is null * ? I * : I * ) * ) */ public function work(callable|null $a = null, callable|null $b = null) : I; } class C { private callable $aCall = fn(int $a): string => (string) $a; private callable $bCall = fn(bool $b): bool => $b; /** * @param I $i * * @return I */ public function nulls(I $i) : I { return $i->work(null, null); } /** * @param I $i * * @return I */ public function a_b(I $i) : I { return $i->work($this->aCall, $this->bCall); } /** * @param I $i * * @return I */ function a(I $i) : I { return $i->work($this->aCall, null); } /** * @param I $i * * @return I */ function b(I $i) : I { return $i->work(null, $this->bCall); } } ``` ``` Psalm output (using commit 3046468): INFO: MixedArgumentTypeCoercion - 53:25 - Argument 1 of I::work expects callable(int):mixed|null, parent type callable provided INFO: MixedArgumentTypeCoercion - 53:39 - Argument 2 of I::work expects callable(mixed):mixed|null, parent type callable provided ERROR: InvalidReturnStatement - 53:16 - The inferred type 'I|I' does not match the declared return type 'I' for C::a_b ERROR: InvalidReturnType - 49:16 - The declared return type 'I' for C::a_b is incorrect, got 'I|I' INFO: MixedArgumentTypeCoercion - 63:25 - Argument 1 of I::work expects callable(int):mixed|null, parent type callable provided ERROR: InvalidReturnStatement - 63:16 - The inferred type 'I|I' does not match the declared return type 'I' for C::a ERROR: InvalidReturnType - 59:16 - The declared return type 'I' for C::a is incorrect, got 'I|I' INFO: MixedArgumentTypeCoercion - 73:31 - Argument 2 of I::work expects callable(mixed):mixed|null, parent type callable provided ERROR: InvalidReturnStatement - 73:16 - The inferred type 'I|I' does not match the declared return type 'I' for C::b ERROR: InvalidReturnType - 69:16 - The declared return type 'I' for C::b is incorrect, got 'I|I' ```
simPod commented 3 years ago

Promise example (currently impossible to type)

/** @template T */
interface Promise
{
    /**
     * @template TResult1
     * @template TResult2
     * @template-default TResult1=T
     * @template-default TResult2=never
     *
     * @param    (callable(T): TResult1)|null $a
     * @param    (callable(mixed): TResult2)|null $b
     *
     * @return
     */
    public function then(callable|null $a = null, callable|null $b = null) : Promise;
}

class C {
    private callable $aCall = fn(int $a): string => (string) $a;
    private callable $bCall = fn(bool $b): bool => $b;

    /**
     * @param Promise<int> $promise
     *
     * @return Promise<int>
     */
    public function nulls(Promise $promise) : Promise
    {
        return $promise->then(null, null);
    }

    /**
     * @param Promise<int> $promise
     *
     * @return Promise<string|bool>
     */
    public function a_b(Promise $promise) : Promise
    {
        return $promise->then($this->aCall, $this->bCall);
    }

    /**
     * @param Promise<int> $promise
     *
     * @return Promise<string>
     */
    function a(Promise $promise) : Promise
    {
        return $promise->then($this->aCall, null);
    }

    /**
     * @param Promise<int> $promise
     *
     * @return Promise<bool>
     */
    function b(Promise $promise) : Promise
    {
        return $promise->then(null, $this->bCall);
    }
}

Conditional types are not a thing for this. It's crazy complicated to write something like that using them. I guess that's also the reason why it does not work in psalm now.

/**
 * @template T
 */
interface Promise
{
    /**
     * @template TResult1
     * @template TResult2
     *
     * @param    (callable(T): TResult1)|null $a
     * @param    (callable(mixed): TResult2)|null $b
     *
     * @return (
     *      $a is null
     *      ? (
     *          $b is null
     *          ? Promise<T>
     *          : Promise<TResult2>
     *         )
     *      : (
     *         $b is null
     *         ? Promise<TResult1>
     *         : Promise<TResult1|TResult2>
     *      )
     * )
     */
    public function then(callable|null $a = null, callable|null $b = null) : Promise;
}
ondrejmirtes commented 1 month ago

This has just been added to PHPStan with the following syntax:

<?php

/**
 * @template T
 * @template U = true
 */
class Foo
{

}