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

Add support for generic CallableType. #2938

Merged
merged 2 commits into from
Feb 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,11 @@ parameters:
count: 2
path: src/PhpDoc/TypeNodeResolver.php

-
message: "#^Property PHPStan\\\\PhpDocParser\\\\Ast\\\\Type\\\\CallableTypeNode\\:\\:\\$templateTypes \\(array\\<PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\TemplateTagValueNode\\>\\) on left side of \\?\\? is not nullable\\.$#"
count: 1
path: src/PhpDoc/TypeNodeResolver.php

-
message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\Identifier\\\\Exception\\\\InvalidIdentifierName is never thrown in the try block\\.$#"
count: 3
Expand Down
35 changes: 33 additions & 2 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PhpParser\Node\Name;
use PHPStan\Analyser\ConstantResolver;
use PHPStan\Analyser\NameScope;
use PHPStan\PhpDoc\Tag\TemplateTag;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode;
Expand Down Expand Up @@ -66,6 +67,9 @@
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeFactory;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeScope;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Helper\GetTemplateTypeType;
use PHPStan\Type\IntegerRangeType;
Expand Down Expand Up @@ -873,7 +877,32 @@ static function (string $variance): TemplateTypeVariance {

private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $nameScope): Type
{
$templateTags = [];

if (count($typeNode->templateTypes ?? []) > 0) {
foreach ($typeNode->templateTypes as $templateType) {
$templateTags[$templateType->name] = new TemplateTag(
$templateType->name,
$templateType->bound !== null
? $this->resolve($templateType->bound, $nameScope)
: new MixedType(),
TemplateTypeVariance::createInvariant(),
);
}
$templateTypeScope = TemplateTypeScope::createWithAnonymousFunction();

$templateTypeMap = new TemplateTypeMap(array_map(
static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag),
$templateTags,
));

$nameScope = $nameScope->withTemplateTypeMap($templateTypeMap);
} else {
$templateTypeMap = TemplateTypeMap::createEmpty();
}

$mainType = $this->resolve($typeNode->identifier, $nameScope);

$isVariadic = false;
$parameters = array_map(
function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadic): NativeParameterReflection {
Expand All @@ -882,6 +911,7 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi
if (str_starts_with($parameterName, '$')) {
$parameterName = substr($parameterName, 1);
}

return new NativeParameterReflection(
$parameterName,
$parameterNode->isOptional || $parameterNode->isVariadic,
Expand All @@ -893,16 +923,17 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi
},
$typeNode->parameters,
);

$returnType = $this->resolve($typeNode->returnType, $nameScope);

if ($mainType instanceof CallableType) {
return new CallableType($parameters, $returnType, $isVariadic);
return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap);

} elseif (
$mainType instanceof ObjectType
&& $mainType->getClassName() === Closure::class
) {
return new ClosureType($parameters, $returnType, $isVariadic);
return new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap);
}

return new ErrorType();
Expand Down
33 changes: 31 additions & 2 deletions src/Type/CallableType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Php\PhpVersion;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
Expand Down Expand Up @@ -54,6 +55,10 @@ class CallableType implements CompoundType, ParametersAcceptor

private bool $isCommonCallable;

private TemplateTypeMap $templateTypeMap;

private TemplateTypeMap $resolvedTemplateTypeMap;

/**
* @api
* @param array<int, ParameterReflection>|null $parameters
Expand All @@ -62,11 +67,15 @@ public function __construct(
?array $parameters = null,
?Type $returnType = null,
private bool $variadic = true,
?TemplateTypeMap $templateTypeMap = null,
?TemplateTypeMap $resolvedTemplateTypeMap = null,
)
{
$this->parameters = $parameters ?? [];
$this->returnType = $returnType ?? new MixedType();
$this->isCommonCallable = $parameters === null && $returnType === null;
$this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty();
$this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty();
}

/**
Expand Down Expand Up @@ -191,6 +200,8 @@ function (): string {
), $this->parameters),
$this->returnType,
$this->variadic,
$this->templateTypeMap,
$this->resolvedTemplateTypeMap,
);

return $printer->print($selfWithoutParameterNames->toPhpDocNode());
Expand Down Expand Up @@ -243,12 +254,12 @@ public function toArrayKey(): Type

public function getTemplateTypeMap(): TemplateTypeMap
{
return TemplateTypeMap::createEmpty();
return $this->templateTypeMap;
}

public function getResolvedTemplateTypeMap(): TemplateTypeMap
{
return TemplateTypeMap::createEmpty();
return $this->resolvedTemplateTypeMap;
}

public function getCallSiteVarianceMap(): TemplateTypeVarianceMap
Expand Down Expand Up @@ -356,6 +367,8 @@ public function traverse(callable $cb): Type
$parameters,
$cb($this->getReturnType()),
$this->isVariadic(),
$this->templateTypeMap,
$this->resolvedTemplateTypeMap,
);
}

Expand Down Expand Up @@ -402,6 +415,8 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
$parameters,
$cb($this->getReturnType(), $rightAcceptors[0]->getReturnType()),
$this->isVariadic(),
$this->templateTypeMap,
$this->resolvedTemplateTypeMap,
);
}

Expand Down Expand Up @@ -552,10 +567,24 @@ public function toPhpDocNode(): TypeNode
);
}

$templateTags = [];
foreach ($this->templateTypeMap->getTypes() as $templateName => $templateType) {
if (!$templateType instanceof TemplateType) {
throw new ShouldNotHappenException();
}

$templateTags[] = new TemplateTagValueNode(
$templateName,
$templateType->getBound()->toPhpDocNode(),
'',
);
}

return new CallableTypeNode(
new IdentifierTypeNode('callable'),
$parameters,
$this->returnType->toPhpDocNode(),
$templateTags,
);
}

Expand Down
16 changes: 16 additions & 0 deletions src/Type/ClosureType.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Closure;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Php\PhpVersion;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
Expand All @@ -23,6 +24,7 @@
use PHPStan\Reflection\PropertyReflection;
use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantBooleanType;
Expand Down Expand Up @@ -618,10 +620,24 @@ public function toPhpDocNode(): TypeNode
);
}

$templateTags = [];
foreach ($this->templateTypeMap->getTypes() as $templateName => $templateType) {
if (!$templateType instanceof TemplateType) {
throw new ShouldNotHappenException();
}

$templateTags[] = new TemplateTagValueNode(
$templateName,
$templateType->getBound()->toPhpDocNode(),
'',
);
}

return new CallableTypeNode(
new IdentifierTypeNode('Closure'),
$parameters,
$this->returnType->toPhpDocNode(),
$templateTags,
);
}

Expand Down
30 changes: 29 additions & 1 deletion src/Type/Generic/TemplateTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Type\Generic;

use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Type\ErrorType;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\NonAcceptingNeverType;
Expand Down Expand Up @@ -85,8 +86,35 @@ public static function resolveToBounds(Type $type): Type
*/
public static function toArgument(Type $type): Type
{
$ownedTemplates = [];

/** @var T */
return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type {
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$ownedTemplates): Type {
if ($type instanceof ParametersAcceptor) {
$templateTypeMap = $type->getTemplateTypeMap();

foreach ($type->getParameters() as $parameter) {
$parameterType = $parameter->getType();
if (!($parameterType instanceof TemplateType) || !$templateTypeMap->hasType($parameterType->getName())) {
continue;
}

$ownedTemplates[] = $parameterType;
}

$returnType = $type->getReturnType();

if ($returnType instanceof TemplateType && $templateTypeMap->hasType($returnType->getName())) {
$ownedTemplates[] = $returnType;
}
}

foreach ($ownedTemplates as $ownedTemplate) {
if ($ownedTemplate === $type) {
return $traverse($type);
}
}

if ($type instanceof TemplateType) {
return $traverse($type->toArgument());
}
Expand Down
9 changes: 9 additions & 0 deletions src/Type/Generic/TemplateTypeScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
class TemplateTypeScope
{

public static function createWithAnonymousFunction(): self
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure at all about this but wasn't sure of an alternative

{
return new self(null, null);
}

public static function createWithFunction(string $functionName): self
{
return new self(null, $functionName);
Expand Down Expand Up @@ -48,6 +53,10 @@ public function equals(self $other): bool
/** @api */
public function describe(): string
{
if ($this->className === null && $this->functionName === null) {
return 'anonymous function';
}

if ($this->className === null) {
return sprintf('function %s()', $this->functionName);
}
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function dataFileAsserts(): iterable
require_once __DIR__ . '/data/bug2574.php';

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug2574.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-callables.php');

require_once __DIR__ . '/data/bug2577.php';

Expand Down
80 changes: 80 additions & 0 deletions tests/PHPStan/Analyser/data/generic-callables.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace GenericCallables;

use Closure;

use function PHPStan\Testing\assertType;

/**
* @template TFuncRet of mixed
* @param TFuncRet $mixed
*
* @return Closure(): TFuncRet
*/
function testFuncClosure(mixed $mixed): Closure
{
}

/**
* @template TFuncRet of mixed
* @param TFuncRet $mixed
*
* @return Closure<TClosureRet of mixed>(TClosureRet $val): (TClosureRet|TFuncRet)
*/
function testFuncClosureMixed(mixed $mixed)
{
}

/**
* @template TFuncRet of mixed
* @param TFuncRet $mixed
*
* @return callable(): TFuncRet
*/
function testFuncCallable(mixed $mixed): callable
{
}

/**
* @param Closure<TRet of mixed>(TRet $val): TRet $callable
* @param non-empty-list<Closure<TRet of mixed>(TRet $val): TRet> $callables
*/
function testClosure(Closure $callable, int $int, string $str, array $callables): void
{
assertType('Closure<TRet of mixed>(TRet): TRet', $callable);
assertType('int', $callable($int));
assertType('string', $callable($str));
assertType('string', $callables[0]($str));
assertType('Closure(): 1', testFuncClosure(1));
}

function testClosureMixed(int $int, string $str): void
{
$closure = testFuncClosureMixed($int);
assertType('Closure<TClosureRet of mixed>(TClosureRet): (int|TClosureRet)', $closure);
assertType('int|string', $closure($str));
}

/**
* @param callable<TRet of mixed>(TRet $val): TRet $callable
*/
function testCallable(callable $callable, int $int, string $str): void
{
assertType('callable<TRet of mixed>(TRet): TRet', $callable);
assertType('int', $callable($int));
assertType('string', $callable($str));
assertType('callable(): 1', testFuncCallable(1));
}

/**
* @param Closure<TRetFirst of mixed>(TRetFirst $valone): (Closure<TRetSecond of mixed>(TRetSecond $valtwo): (TRetFirst|TRetSecond)) $closure
*/
function testNestedClosures(Closure $closure, string $str, int $int): void
{
assertType('Closure<TRetFirst of mixed>(TRetFirst): (Closure<TRetSecond of mixed>(TRetSecond $valtwo): (TRetFirst|TRetSecond))', $closure);
Copy link
Contributor Author

@mad-briller mad-briller Feb 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the return type Closure does not have the parameter names removed as the removal is done in describe() not toPhpDocNode(), and describe is only called for the top-level. not sure if this is intended or not so thought i'd mention it

$closure1 = $closure($str);
assertType('Closure<TRetSecond of mixed>(TRetSecond): (string|TRetSecond)', $closure1);
$result = $closure1($int);
assertType('int|string', $result);
}
Loading