Skip to content

Commit 5ac0fb2

Browse files
authored
Add support for generic CallableType
1 parent 686b4c7 commit 5ac0fb2

File tree

8 files changed

+204
-5
lines changed

8 files changed

+204
-5
lines changed

phpstan-baseline.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ parameters:
267267
count: 2
268268
path: src/PhpDoc/TypeNodeResolver.php
269269

270+
-
271+
message: "#^Property PHPStan\\\\PhpDocParser\\\\Ast\\\\Type\\\\CallableTypeNode\\:\\:\\$templateTypes \\(array\\<PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\TemplateTagValueNode\\>\\) on left side of \\?\\? is not nullable\\.$#"
272+
count: 1
273+
path: src/PhpDoc/TypeNodeResolver.php
274+
270275
-
271276
message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\Identifier\\\\Exception\\\\InvalidIdentifierName is never thrown in the try block\\.$#"
272277
count: 3

src/PhpDoc/TypeNodeResolver.php

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PhpParser\Node\Name;
1111
use PHPStan\Analyser\ConstantResolver;
1212
use PHPStan\Analyser\NameScope;
13+
use PHPStan\PhpDoc\Tag\TemplateTag;
1314
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
1415
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode;
1516
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode;
@@ -66,6 +67,9 @@
6667
use PHPStan\Type\Generic\GenericClassStringType;
6768
use PHPStan\Type\Generic\GenericObjectType;
6869
use PHPStan\Type\Generic\TemplateType;
70+
use PHPStan\Type\Generic\TemplateTypeFactory;
71+
use PHPStan\Type\Generic\TemplateTypeMap;
72+
use PHPStan\Type\Generic\TemplateTypeScope;
6973
use PHPStan\Type\Generic\TemplateTypeVariance;
7074
use PHPStan\Type\Helper\GetTemplateTypeType;
7175
use PHPStan\Type\IntegerRangeType;
@@ -873,7 +877,32 @@ static function (string $variance): TemplateTypeVariance {
873877

874878
private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $nameScope): Type
875879
{
880+
$templateTags = [];
881+
882+
if (count($typeNode->templateTypes ?? []) > 0) {
883+
foreach ($typeNode->templateTypes as $templateType) {
884+
$templateTags[$templateType->name] = new TemplateTag(
885+
$templateType->name,
886+
$templateType->bound !== null
887+
? $this->resolve($templateType->bound, $nameScope)
888+
: new MixedType(),
889+
TemplateTypeVariance::createInvariant(),
890+
);
891+
}
892+
$templateTypeScope = TemplateTypeScope::createWithAnonymousFunction();
893+
894+
$templateTypeMap = new TemplateTypeMap(array_map(
895+
static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag),
896+
$templateTags,
897+
));
898+
899+
$nameScope = $nameScope->withTemplateTypeMap($templateTypeMap);
900+
} else {
901+
$templateTypeMap = TemplateTypeMap::createEmpty();
902+
}
903+
876904
$mainType = $this->resolve($typeNode->identifier, $nameScope);
905+
877906
$isVariadic = false;
878907
$parameters = array_map(
879908
function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadic): NativeParameterReflection {
@@ -882,6 +911,7 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi
882911
if (str_starts_with($parameterName, '$')) {
883912
$parameterName = substr($parameterName, 1);
884913
}
914+
885915
return new NativeParameterReflection(
886916
$parameterName,
887917
$parameterNode->isOptional || $parameterNode->isVariadic,
@@ -893,16 +923,17 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi
893923
},
894924
$typeNode->parameters,
895925
);
926+
896927
$returnType = $this->resolve($typeNode->returnType, $nameScope);
897928

898929
if ($mainType instanceof CallableType) {
899-
return new CallableType($parameters, $returnType, $isVariadic);
930+
return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap);
900931

901932
} elseif (
902933
$mainType instanceof ObjectType
903934
&& $mainType->getClassName() === Closure::class
904935
) {
905-
return new ClosureType($parameters, $returnType, $isVariadic);
936+
return new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap);
906937
}
907938

908939
return new ErrorType();

src/Type/CallableType.php

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\Analyser\OutOfClassScope;
66
use PHPStan\Php\PhpVersion;
7+
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
78
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
89
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
910
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
@@ -54,6 +55,10 @@ class CallableType implements CompoundType, ParametersAcceptor
5455

5556
private bool $isCommonCallable;
5657

58+
private TemplateTypeMap $templateTypeMap;
59+
60+
private TemplateTypeMap $resolvedTemplateTypeMap;
61+
5762
/**
5863
* @api
5964
* @param array<int, ParameterReflection>|null $parameters
@@ -62,11 +67,15 @@ public function __construct(
6267
?array $parameters = null,
6368
?Type $returnType = null,
6469
private bool $variadic = true,
70+
?TemplateTypeMap $templateTypeMap = null,
71+
?TemplateTypeMap $resolvedTemplateTypeMap = null,
6572
)
6673
{
6774
$this->parameters = $parameters ?? [];
6875
$this->returnType = $returnType ?? new MixedType();
6976
$this->isCommonCallable = $parameters === null && $returnType === null;
77+
$this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty();
78+
$this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty();
7079
}
7180

7281
/**
@@ -191,6 +200,8 @@ function (): string {
191200
), $this->parameters),
192201
$this->returnType,
193202
$this->variadic,
203+
$this->templateTypeMap,
204+
$this->resolvedTemplateTypeMap,
194205
);
195206

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

244255
public function getTemplateTypeMap(): TemplateTypeMap
245256
{
246-
return TemplateTypeMap::createEmpty();
257+
return $this->templateTypeMap;
247258
}
248259

249260
public function getResolvedTemplateTypeMap(): TemplateTypeMap
250261
{
251-
return TemplateTypeMap::createEmpty();
262+
return $this->resolvedTemplateTypeMap;
252263
}
253264

254265
public function getCallSiteVarianceMap(): TemplateTypeVarianceMap
@@ -356,6 +367,8 @@ public function traverse(callable $cb): Type
356367
$parameters,
357368
$cb($this->getReturnType()),
358369
$this->isVariadic(),
370+
$this->templateTypeMap,
371+
$this->resolvedTemplateTypeMap,
359372
);
360373
}
361374

@@ -402,6 +415,8 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
402415
$parameters,
403416
$cb($this->getReturnType(), $rightAcceptors[0]->getReturnType()),
404417
$this->isVariadic(),
418+
$this->templateTypeMap,
419+
$this->resolvedTemplateTypeMap,
405420
);
406421
}
407422

@@ -552,10 +567,24 @@ public function toPhpDocNode(): TypeNode
552567
);
553568
}
554569

570+
$templateTags = [];
571+
foreach ($this->templateTypeMap->getTypes() as $templateName => $templateType) {
572+
if (!$templateType instanceof TemplateType) {
573+
throw new ShouldNotHappenException();
574+
}
575+
576+
$templateTags[] = new TemplateTagValueNode(
577+
$templateName,
578+
$templateType->getBound()->toPhpDocNode(),
579+
'',
580+
);
581+
}
582+
555583
return new CallableTypeNode(
556584
new IdentifierTypeNode('callable'),
557585
$parameters,
558586
$this->returnType->toPhpDocNode(),
587+
$templateTags,
559588
);
560589
}
561590

src/Type/ClosureType.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Closure;
66
use PHPStan\Analyser\OutOfClassScope;
77
use PHPStan\Php\PhpVersion;
8+
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
89
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
910
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
1011
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
@@ -23,6 +24,7 @@
2324
use PHPStan\Reflection\PropertyReflection;
2425
use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
2526
use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
27+
use PHPStan\ShouldNotHappenException;
2628
use PHPStan\TrinaryLogic;
2729
use PHPStan\Type\Constant\ConstantArrayType;
2830
use PHPStan\Type\Constant\ConstantBooleanType;
@@ -618,10 +620,24 @@ public function toPhpDocNode(): TypeNode
618620
);
619621
}
620622

623+
$templateTags = [];
624+
foreach ($this->templateTypeMap->getTypes() as $templateName => $templateType) {
625+
if (!$templateType instanceof TemplateType) {
626+
throw new ShouldNotHappenException();
627+
}
628+
629+
$templateTags[] = new TemplateTagValueNode(
630+
$templateName,
631+
$templateType->getBound()->toPhpDocNode(),
632+
'',
633+
);
634+
}
635+
621636
return new CallableTypeNode(
622637
new IdentifierTypeNode('Closure'),
623638
$parameters,
624639
$this->returnType->toPhpDocNode(),
640+
$templateTags,
625641
);
626642
}
627643

src/Type/Generic/TemplateTypeHelper.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Type\Generic;
44

5+
use PHPStan\Reflection\ParametersAcceptor;
56
use PHPStan\Type\ErrorType;
67
use PHPStan\Type\GeneralizePrecision;
78
use PHPStan\Type\NonAcceptingNeverType;
@@ -85,8 +86,35 @@ public static function resolveToBounds(Type $type): Type
8586
*/
8687
public static function toArgument(Type $type): Type
8788
{
89+
$ownedTemplates = [];
90+
8891
/** @var T */
89-
return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type {
92+
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$ownedTemplates): Type {
93+
if ($type instanceof ParametersAcceptor) {
94+
$templateTypeMap = $type->getTemplateTypeMap();
95+
96+
foreach ($type->getParameters() as $parameter) {
97+
$parameterType = $parameter->getType();
98+
if (!($parameterType instanceof TemplateType) || !$templateTypeMap->hasType($parameterType->getName())) {
99+
continue;
100+
}
101+
102+
$ownedTemplates[] = $parameterType;
103+
}
104+
105+
$returnType = $type->getReturnType();
106+
107+
if ($returnType instanceof TemplateType && $templateTypeMap->hasType($returnType->getName())) {
108+
$ownedTemplates[] = $returnType;
109+
}
110+
}
111+
112+
foreach ($ownedTemplates as $ownedTemplate) {
113+
if ($ownedTemplate === $type) {
114+
return $traverse($type);
115+
}
116+
}
117+
90118
if ($type instanceof TemplateType) {
91119
return $traverse($type->toArgument());
92120
}

src/Type/Generic/TemplateTypeScope.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
class TemplateTypeScope
88
{
99

10+
public static function createWithAnonymousFunction(): self
11+
{
12+
return new self(null, null);
13+
}
14+
1015
public static function createWithFunction(string $functionName): self
1116
{
1217
return new self(null, $functionName);
@@ -48,6 +53,10 @@ public function equals(self $other): bool
4853
/** @api */
4954
public function describe(): string
5055
{
56+
if ($this->className === null && $this->functionName === null) {
57+
return 'anonymous function';
58+
}
59+
5160
if ($this->className === null) {
5261
return sprintf('function %s()', $this->functionName);
5362
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public function dataFileAsserts(): iterable
2727
require_once __DIR__ . '/data/bug2574.php';
2828

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

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace GenericCallables;
4+
5+
use Closure;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @template TFuncRet of mixed
11+
* @param TFuncRet $mixed
12+
*
13+
* @return Closure(): TFuncRet
14+
*/
15+
function testFuncClosure(mixed $mixed): Closure
16+
{
17+
}
18+
19+
/**
20+
* @template TFuncRet of mixed
21+
* @param TFuncRet $mixed
22+
*
23+
* @return Closure<TClosureRet of mixed>(TClosureRet $val): (TClosureRet|TFuncRet)
24+
*/
25+
function testFuncClosureMixed(mixed $mixed)
26+
{
27+
}
28+
29+
/**
30+
* @template TFuncRet of mixed
31+
* @param TFuncRet $mixed
32+
*
33+
* @return callable(): TFuncRet
34+
*/
35+
function testFuncCallable(mixed $mixed): callable
36+
{
37+
}
38+
39+
/**
40+
* @param Closure<TRet of mixed>(TRet $val): TRet $callable
41+
* @param non-empty-list<Closure<TRet of mixed>(TRet $val): TRet> $callables
42+
*/
43+
function testClosure(Closure $callable, int $int, string $str, array $callables): void
44+
{
45+
assertType('Closure<TRet of mixed>(TRet): TRet', $callable);
46+
assertType('int', $callable($int));
47+
assertType('string', $callable($str));
48+
assertType('string', $callables[0]($str));
49+
assertType('Closure(): 1', testFuncClosure(1));
50+
}
51+
52+
function testClosureMixed(int $int, string $str): void
53+
{
54+
$closure = testFuncClosureMixed($int);
55+
assertType('Closure<TClosureRet of mixed>(TClosureRet): (int|TClosureRet)', $closure);
56+
assertType('int|string', $closure($str));
57+
}
58+
59+
/**
60+
* @param callable<TRet of mixed>(TRet $val): TRet $callable
61+
*/
62+
function testCallable(callable $callable, int $int, string $str): void
63+
{
64+
assertType('callable<TRet of mixed>(TRet): TRet', $callable);
65+
assertType('int', $callable($int));
66+
assertType('string', $callable($str));
67+
assertType('callable(): 1', testFuncCallable(1));
68+
}
69+
70+
/**
71+
* @param Closure<TRetFirst of mixed>(TRetFirst $valone): (Closure<TRetSecond of mixed>(TRetSecond $valtwo): (TRetFirst|TRetSecond)) $closure
72+
*/
73+
function testNestedClosures(Closure $closure, string $str, int $int): void
74+
{
75+
assertType('Closure<TRetFirst of mixed>(TRetFirst): (Closure<TRetSecond of mixed>(TRetSecond $valtwo): (TRetFirst|TRetSecond))', $closure);
76+
$closure1 = $closure($str);
77+
assertType('Closure<TRetSecond of mixed>(TRetSecond): (string|TRetSecond)', $closure1);
78+
$result = $closure1($int);
79+
assertType('int|string', $result);
80+
}

0 commit comments

Comments
 (0)