phpstan icon indicating copy to clipboard operation
phpstan copied to clipboard

Generic's sub-type is lost when passed through a callable

Open gnutix opened this issue 3 years ago • 7 comments

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: )

gnutix avatar May 21 '22 12:05 gnutix

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.

rvanvelzen avatar May 30 '22 12:05 rvanvelzen

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);
    }
}

gnutix avatar May 30 '22 13:05 gnutix

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 avatar Jun 01 '22 06:06 rvanvelzen

@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 avatar Aug 07 '22 11:08 gnutix

@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 avatar Aug 16 '22 10:08 rvanvelzen

@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)

gnutix avatar Sep 28 '23 11:09 gnutix