vimeo / psalm

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

Keep track of templates for callables #4589

Open thomasvargiu opened 3 years ago

thomasvargiu commented 3 years ago

Consider the following two examples. I think it should be possible to keep track of template references and infer the right type of the return type.

https://psalm.dev/r/6e44e1eccf https://psalm.dev/r/4efcc7e309

/**
 * @template B
 * @psalm-return callable(B): B
 */
function foo(): callable
{
    return function ($fab) {
        return $fab;
    };
}

For example, in the above code, as the interal psalm code does, function foo() should return a callable(B:fn-foo as mixed): B:fn-foo as mixed, so that calling this callable we know that the return type is the first argument.

Same in the following code:


/**
 * @template T
 * @template B
 * @psalm-param T $e
 * @psalm-return callable(callable(T): B): B
 */
function foo($e): callable
{
    return function ($fab) use ($e) {
        return $fab($e);
    };
}

Here, nested callables should keep track of the template.

Without this improvement, it's almost impossible to work with nested callables, specially developing or documenting a functional programming library.

I think it should be possible, right?

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

I found these snippets:

https://psalm.dev/r/6e44e1eccf ```php strlen($a)); ``` ``` Psalm output (using commit fda2377): ERROR: InvalidScalarArgument - 17:12 - Argument 1 expects callable(string(foo)):empty, pure-Closure(string):int provided ```
https://psalm.dev/r/4efcc7e309 ```php
thomasvargiu commented 3 years ago

Looking at this example: https://psalm.dev/r/b6c1411ec4

I think it should be possible.

  1. fist call foo('foo') should return callable(string(foo)): B:fn-foo
  2. B-foo should continue to be a templated type because is recognized as a deferred type, because it's a result of a parameter (the callable)
  3. second call should accepts a callable(string(foo)): B:fn-foo and infer return type from the caller
  4. same logic should be recursive

@weirdan what do you think?

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

I found these snippets:

https://psalm.dev/r/b6c1411ec4 ```php strlen($a)); ``` ``` Psalm output (using commit 74c07bb): ERROR: InvalidScalarArgument - 17:12 - Argument 1 expects callable(string(foo)):empty, pure-Closure(string):int provided ```
weirdan commented 3 years ago

That would mean foo should return a generic callable (something like callable<T of mixed>(callable(string(foo)): T): T) and to my knowledge Psalm does not support generic callables at the moment. Technically this does typecheck and run (https://psalm.dev/r/b618884527, https://3v4l.org/EHf6N) when rewritten using a class, so the question is all about the docblock syntax.

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

I found these snippets:

https://psalm.dev/r/b618884527 ```php e = $e; } /** * @template B * @param callable(T): B $p * @return B */ public function __invoke($p) { return $p($this->e); } } /** * @template T * @psalm-param T $e * @return myInvokable */ function foo($e): callable { return new myInvokable($e); } $_z = foo('foo')(fn (string $a) => strlen($a)); /** @psalm-trace $_z */; // var_dump($_z); ``` ``` Psalm output (using commit 74c07bb): INFO: Trace - 34:24 - $_z: int ```
thomasvargiu commented 3 years ago

In your example you are wrapping almost the same logic in a invokable class, but with the same behaviour. But it's impossible with already existing librararies, and excessive to creare a class for a simple lambda function, and I think it's something that will be use more in the next years starting from PHP 7.4 with arrow functions. I was trying to do it writing a plugin (FunctionReturnTypeProviderInterface) for a pipe() function (composition of one or more callables, pipe(f1, f2, f3)($a)), returning a chain of callables keeping the "generics, and then evaluating calls with aAfterStatementAnalysisInterface`, revaluating variables in context. Obviously I was just trying and playing with it, but with more knolodgment I think it would be possible to make a plugin for it (at least for a specific function).

Now, a function like that will use empty and not even mixed, so it's a little bit complicated to use it writing the right return type.

weirdan commented 3 years ago

In your example you are wrapping almost the same logic in a invokable class, but with the same behaviour. But it's impossible with already existing librararies

Indeed. The point of this exercise was to see how it maps to concepts already used in Psalm and to check if it's sound. I'm not suggesting you to actually rewrite anything.

thomasvargiu commented 3 years ago

Oh, maybe as the first thing, should psalm support templates in closures? Or I'm doing something wrong here? https://psalm.dev/r/79c407cbf4

That's why my experimental plugin wasn't working :)

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

I found these snippets:

https://psalm.dev/r/79c407cbf4 ```php
thomasvargiu commented 3 years ago

Probably related to #4550