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

Impossible types are not excluded from inferred template type #7889

Open
thomasvargiu opened this issue Apr 22, 2022 · 6 comments
Open

Impossible types are not excluded from inferred template type #7889

thomasvargiu opened this issue Apr 22, 2022 · 6 comments

Comments

@thomasvargiu
Copy link
Contributor

With this example we can see that some param values aren't possibile, but the R template value is always assigned, probably because it's not inferred from the whole sum type.

Expected: the R template should have the union of only possibile types.

https://psalm.dev/r/6c26fe7cfb

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/6c26fe7cfb
<?php

/**
 * @psalm-suppress InvalidReturnType
 * @psalm-suppress UnusedParam
 *
 * @template A1
 * @template A2
 * @template A3
 * @template R
 * @param (
 *   (array<string> & array{callable(A1): A2, callable(A2): A3, callable(A3): R})
 *   | array{callable(A1): A2, callable(A2): R}
 *   | (array{2: null} & array{callable(A1): R})
 * ) $callbacks
 * @psalm-return callable(A1): R
 */
function compose(array $callbacks): callable
{
}

/** @psalm-trace $a */
$a = compose([
    fn (int $a): string => 'bar',
    fn (string $a): string => 'foo',
    fn (string $a): int => 7,
]);

/** @psalm-trace $b */
$b = $a(3);
Psalm output (using commit b5b5c20):

INFO: UnusedClosureParam - 24:13 - Param $a is never referenced in this method

INFO: UnusedClosureParam - 25:16 - Param $a is never referenced in this method

INFO: UnusedClosureParam - 26:16 - Param $a is never referenced in this method

INFO: Trace - 23:1 - $a: callable(int):('bar'|'foo'|7)

INFO: Trace - 30:1 - $b: 'bar'|'foo'|7

INFO: UnusedVariable - 30:1 - $b is never referenced or the value is not used

@AndrolGenhald
Copy link
Collaborator

AndrolGenhald commented Apr 22, 2022

Part of the problem here is that array shapes are not exhaustive, so the array passed to compose() can match array{callable(A1): R}, array{callable(A1): A2, callable(A2): R}, and array{callable(A1): A2, callable(A2): A3, callable(A3): R} all at the same time. Until we have a way to specify array shapes that don't allow extra keys this has no chance of working.

If you're ok using a plugin for this sort of thing though, both @klimick and @veewee did some work on pipe stuff like this recently, I'm not sure if either of them ended up with a working plugin though.

@AndrolGenhald
Copy link
Collaborator

Without using a plugin this almost works, but for some reason it resolves A1 to mixed. It's also incredibly verbose, and will get much more verbose if you want to allow more callbacks. I think a plugin is really the way to go here.

@psalm-github-bot
Copy link

I found these snippets:

https://psalm.dev/r/ada2824820
<?php

/**
 * @psalm-suppress InvalidReturnType
 * @psalm-suppress UnusedParam
 *
 * @template A1
 * @template A2
 * @template A3
 * @template R1
 * @template R2
 * @template R3
 * @template C of (
 *   array{callable(A1): A2, callable(A2): A3, callable(A3): R3}
 *   | array{callable(A1): A2, callable(A2): R2}
 *   | array{callable(A1): R1}
 * )
 * @param C $callbacks
 * @psalm-return (
 *     C is array{callable(A1): A2, callable(A2): A3, callable(A3): R3} ? (callable(A1): R3) : (
 *         C is array{callable(A1): A2, callable(A2): R2} ? (callable(A1): R2) : (
 *             (callable(A1): R1)
 *         )
 *     )
 * )
 */
function compose(array $callbacks): callable
{
}

/** @psalm-trace $a */
$a = compose([
    fn (int $a): string => 'bar',
    fn (string $a): string => 'foo',
    fn (string $a): int => 7,
]);

/** @psalm-trace $b */
$b = $a(3);
Psalm output (using commit b5b5c20):

INFO: Trace - 32:1 - $a: callable(mixed):7

INFO: Trace - 39:1 - $b: 7

@veewee
Copy link
Contributor

veewee commented Apr 23, 2022

Yes a plugin is the way to go here.
More info and an example of pipe can be found in this PR and linked example code
#7471
#7244 (comment)
https://github.com/klimick/psalm/blob/partiall-application-example/tests/Config/Plugin/Hook/PipeFunctionStorageProvider.php

The result is most likely mixed because of this:
#7244

(Haven't found the time to dive into it deeper)

@thomasvargiu
Copy link
Contributor Author

I'm sorry, but the code of the compose function was just an example.

@AndrolGenhald

Part of the problem here is that array shapes are not exhaustive, so the array passed to compose() can match array{callable(A1): R}, array{callable(A1): A2, callable(A2): R}, and array{callable(A1): A2, callable(A2): A3, callable(A3): R} all at the same time. Until we have a way to specify array shapes that don't allow extra keys this has no chance of working.

You can see that the $callbacks param accept 3 types, and 2 of them are the sum of 2 types (in order to explain the problem):

@param (
 *   (array<string> & array{callable(A1): A2, callable(A2): A3, callable(A3): R})
 *   | array{callable(A1): A2, callable(A2): R}
 *   | (array{2: null} & array{callable(A1): R})
 * ) $callbacks

So, the problem is, I'm calling the function with an array<callable>, but the R template possibile values are assigned even for a type of array<string> & array<callable>.
I think the problem is that template values are inferred from the inner types, without evaluating the whole sum type.

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