Generic's sub-type is lost when passed through a callable
Bug report
Hello there. While adding some types and types' tests for a bunch of Functional-related functions in our codebase, I noticed a loss of precision in the return type of operations like map, reduce and collect, when a Generic with a sub-type is passed through a callable.
I've collected a few playgrounds over the last few weeks, sorry if some of them overlap.
(I don't currently have the time to work on an implementation, but maybe someone will.)
Code snippet that reproduces the problem
- map : https://phpstan.org/r/51907af0-5df5-4e48-a533-6b257e76841d
- collect : https://phpstan.org/r/4bc53446-aa55-417d-8cf8-a9960f90dfe3
- reduce : https://phpstan.org/r/596faeab-51df-409f-9261-ff1407b351e8
Expected output
The expected types should be the ones returned by PHPStan.
Did PHPStan help you today? Did it make you happy in any way?
(I have a bunch of small issues to create, sorry if I don't come up with something new for each of them :pray: )
Your callables are only defined to return Timeline, that's why they don't get more specific.
array_map() has some special handling which makes it understand what happens in the callable slightly better.
If by "special handling", you mean a custom DynamicReturnTypeExtension, then I'm confused, because I've created one too for our map() wrapper which extends PHPStan's (cf. code below), which I could not include in the playground. So I should have exactly the same results as array_map, hypothetically. Or is it somewhere else, hardcoded in PHPStan's internal code ? :thinking:
<?php
declare(strict_types=1);
namespace Gammadia\Collections\PhpStan\Functional\Extensions;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Php\ArrayMapFunctionReturnTypeExtension;
use PHPStan\Type\Type;
use function Gammadia\Collections\Functional\reverse;
final class FunctionalMapDynamicReturnTypeExtension extends ArrayMapFunctionReturnTypeExtension
{
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return 'gammadia\collections\functional\map' === strtolower($functionReflection->getName());
}
/**
* Our implementation as reversed arguments (array first, callable second) compared to array_map
*/
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
$reversedFunctionCall = new FuncCall($functionCall->name, reverse($functionCall->getArgs()));
return parent::getTypeFromFunctionCall($functionReflection, $reversedFunctionCall, $scope);
}
}
Unfortunately I'm not talking about an extension, but internal handling: https://github.com/phpstan/phpstan-src/blob/75c5574a402e858458bf0f83a5942a22e6cfb737/src/Analyser/MutatingScope.php#L1235
@rvanvelzen would it be feasible to make that "internal handling" pluggable ? Like being able to define a list of functions (and/or methods?) acting like array_map ?
@gnutix fairly unlikely, but this case will be solved when generalization of template types goes away (see phpstan/phpstan-src#1206 for some info on that)
@rvanvelzen Out of curiosity, is this "generalization of template types going away" thing still a topic ? (just seen the PR hasn't been touched in a year)