Skip to content

Commit 01f99c1

Browse files
committed
Infer template types from callable parameters properly
1 parent 07d78b2 commit 01f99c1

13 files changed

+323
-19
lines changed

src/Analyser/MutatingScope.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3685,7 +3685,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
36853685
return $length;
36863686
}
36873687

3688-
return ((int) $b['sure']) - ((int) $a['sure']);
3688+
return $b['sure'] - $a['sure']; // @phpstan-ignore-line
36893689
});
36903690

36913691
$scope = $this;

src/Reflection/GenericParametersAcceptorResolver.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc
2727
}
2828

2929
$paramType = $param->getType();
30+
31+
// todo zde "doaplikovat" typy, ktere se dosud nevyskytly - typicky callable(T) - parametr callable
3032
$typeMap = $typeMap->union($paramType->inferTemplateTypes($argType));
3133
}
3234

src/Type/CallableType.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,12 +232,27 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
232232
$typeMap = TemplateTypeMap::createEmpty();
233233

234234
foreach ($parametersAcceptors as $parametersAcceptor) {
235-
$typeMap = $typeMap->union($this->getReturnType()->inferTemplateTypes($parametersAcceptor->getReturnType()));
235+
$typeMap = $typeMap->union($this->inferTemplateTypesOnParametersAcceptor($receivedType, $parametersAcceptor));
236236
}
237237

238238
return $typeMap;
239239
}
240240

241+
private function inferTemplateTypesOnParametersAcceptor(Type $receivedType, ParametersAcceptor $parametersAcceptor): TemplateTypeMap
242+
{
243+
$typeMap = TemplateTypeMap::createEmpty();
244+
$args = $parametersAcceptor->getParameters();
245+
$returnType = $parametersAcceptor->getReturnType();
246+
247+
foreach ($this->getParameters() as $i => $param) {
248+
$argType = isset($args[$i]) ? $args[$i]->getType() : new NeverType();
249+
$paramType = $param->getType();
250+
$typeMap = $typeMap->union($paramType->inferTemplateTypes($argType)->convertToLowerBoundTypes());
251+
}
252+
253+
return $typeMap->union($this->getReturnType()->inferTemplateTypes($returnType));
254+
}
255+
241256
public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
242257
{
243258
$references = $this->getReturnType()->getReferencedTemplateTypes(

src/Type/ClosureType.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,12 +347,27 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
347347
$typeMap = TemplateTypeMap::createEmpty();
348348

349349
foreach ($parametersAcceptors as $parametersAcceptor) {
350-
$typeMap = $typeMap->union($this->getReturnType()->inferTemplateTypes($parametersAcceptor->getReturnType()));
350+
$typeMap = $typeMap->union($this->inferTemplateTypesOnParametersAcceptor($receivedType, $parametersAcceptor));
351351
}
352352

353353
return $typeMap;
354354
}
355355

356+
private function inferTemplateTypesOnParametersAcceptor(Type $receivedType, ParametersAcceptor $parametersAcceptor): TemplateTypeMap
357+
{
358+
$typeMap = TemplateTypeMap::createEmpty();
359+
$args = $parametersAcceptor->getParameters();
360+
$returnType = $parametersAcceptor->getReturnType();
361+
362+
foreach ($this->getParameters() as $i => $param) {
363+
$argType = isset($args[$i]) ? $args[$i]->getType() : new NeverType();
364+
$paramType = $param->getType();
365+
$typeMap = $typeMap->union($paramType->inferTemplateTypes($argType)->convertToLowerBoundTypes());
366+
}
367+
368+
return $typeMap->union($this->getReturnType()->inferTemplateTypes($returnType));
369+
}
370+
356371
public function traverse(callable $cb): Type
357372
{
358373
return new self(

src/Type/Generic/TemplateTypeMap.php

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Type\Generic;
44

55
use PHPStan\Type\MixedType;
6+
use PHPStan\Type\NeverType;
67
use PHPStan\Type\Type;
78
use PHPStan\Type\TypeCombinator;
89
use PHPStan\Type\TypeUtils;
@@ -16,13 +17,36 @@ class TemplateTypeMap
1617
/** @var array<string,\PHPStan\Type\Type> */
1718
private array $types;
1819

20+
/** @var array<string,\PHPStan\Type\Type> */
21+
private array $lowerBoundTypes;
22+
1923
/**
2024
* @api
2125
* @param array<string,\PHPStan\Type\Type> $types
26+
* @param array<string,\PHPStan\Type\Type> $lowerBoundTypes
2227
*/
23-
public function __construct(array $types)
28+
public function __construct(array $types, array $lowerBoundTypes = [])
2429
{
2530
$this->types = $types;
31+
$this->lowerBoundTypes = $lowerBoundTypes;
32+
}
33+
34+
public function convertToLowerBoundTypes(): self
35+
{
36+
$lowerBoundTypes = $this->types;
37+
foreach ($this->lowerBoundTypes as $name => $type) {
38+
if (isset($lowerBoundTypes[$name])) {
39+
$intersection = TypeCombinator::intersect($lowerBoundTypes[$name], $type);
40+
if ($intersection instanceof NeverType) {
41+
continue;
42+
}
43+
$lowerBoundTypes[$name] = $intersection;
44+
} else {
45+
$lowerBoundTypes[$name] = $type;
46+
}
47+
}
48+
49+
return new self([], $lowerBoundTypes);
2650
}
2751

2852
public static function createEmpty(): self
@@ -33,7 +57,7 @@ public static function createEmpty(): self
3357
return $empty;
3458
}
3559

36-
$empty = new self([]);
60+
$empty = new self([], []);
3761
self::$empty = $empty;
3862

3963
return $empty;
@@ -52,17 +76,26 @@ public function count(): int
5276
/** @return array<string,\PHPStan\Type\Type> */
5377
public function getTypes(): array
5478
{
55-
return $this->types;
79+
$types = $this->types;
80+
foreach ($this->lowerBoundTypes as $name => $type) {
81+
if (array_key_exists($name, $types)) {
82+
continue;
83+
}
84+
85+
$types[$name] = $type;
86+
}
87+
88+
return $types;
5689
}
5790

5891
public function hasType(string $name): bool
5992
{
60-
return array_key_exists($name, $this->types);
93+
return array_key_exists($name, $this->getTypes());
6194
}
6295

6396
public function getType(string $name): ?Type
6497
{
65-
return $this->types[$name] ?? null;
98+
return $this->getTypes()[$name] ?? null;
6699
}
67100

68101
public function unsetType(string $name): self
@@ -72,14 +105,16 @@ public function unsetType(string $name): self
72105
}
73106

74107
$types = $this->types;
108+
$lowerBoundTypes = $this->lowerBoundTypes;
75109

76110
unset($types[$name]);
111+
unset($lowerBoundTypes[$name]);
77112

78-
if (count($types) === 0) {
113+
if (count($types) === 0 && count($lowerBoundTypes) === 0) {
79114
return self::createEmpty();
80115
}
81116

82-
return new self($types);
117+
return new self($types, $lowerBoundTypes);
83118
}
84119

85120
public function union(self $other): self
@@ -94,7 +129,20 @@ public function union(self $other): self
94129
}
95130
}
96131

97-
return new self($result);
132+
$resultLowerBoundTypes = $this->lowerBoundTypes;
133+
foreach ($other->lowerBoundTypes as $name => $type) {
134+
if (isset($resultLowerBoundTypes[$name])) {
135+
$intersection = TypeCombinator::intersect($resultLowerBoundTypes[$name], $type);
136+
if ($intersection instanceof NeverType) {
137+
continue;
138+
}
139+
$resultLowerBoundTypes[$name] = $intersection;
140+
} else {
141+
$resultLowerBoundTypes[$name] = $type;
142+
}
143+
}
144+
145+
return new self($result, $resultLowerBoundTypes);
98146
}
99147

100148
public function benevolentUnion(self $other): self
@@ -109,7 +157,20 @@ public function benevolentUnion(self $other): self
109157
}
110158
}
111159

112-
return new self($result);
160+
$resultLowerBoundTypes = $this->lowerBoundTypes;
161+
foreach ($other->lowerBoundTypes as $name => $type) {
162+
if (isset($resultLowerBoundTypes[$name])) {
163+
$intersection = TypeCombinator::intersect($resultLowerBoundTypes[$name], $type);
164+
if ($intersection instanceof NeverType) {
165+
continue;
166+
}
167+
$resultLowerBoundTypes[$name] = $intersection;
168+
} else {
169+
$resultLowerBoundTypes[$name] = $type;
170+
}
171+
}
172+
173+
return new self($result, $resultLowerBoundTypes);
113174
}
114175

115176
public function intersect(self $other): self
@@ -124,14 +185,23 @@ public function intersect(self $other): self
124185
}
125186
}
126187

127-
return new self($result);
188+
$resultLowerBoundTypes = $this->lowerBoundTypes;
189+
foreach ($other->lowerBoundTypes as $name => $type) {
190+
if (isset($resultLowerBoundTypes[$name])) {
191+
$resultLowerBoundTypes[$name] = TypeCombinator::union($resultLowerBoundTypes[$name], $type);
192+
} else {
193+
$resultLowerBoundTypes[$name] = $type;
194+
}
195+
}
196+
197+
return new self($result, $resultLowerBoundTypes);
128198
}
129199

130200
/** @param callable(string,Type):Type $cb */
131201
public function map(callable $cb): self
132202
{
133203
$types = [];
134-
foreach ($this->types as $name => $type) {
204+
foreach ($this->getTypes() as $name => $type) {
135205
$types[$name] = $cb($name, $type);
136206
}
137207

@@ -156,7 +226,8 @@ public function resolveToBounds(): self
156226
public static function __set_state(array $properties): self
157227
{
158228
return new self(
159-
$properties['types']
229+
$properties['types'],
230+
$properties['lowerBoundTypes'] ?? []
160231
);
161232
}
162233

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,10 @@ public function dataFileAsserts(): iterable
416416
yield from $this->gatherAssertTypes(__DIR__ . '/data/empty-array-shape.php');
417417
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5089.php');
418418
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3158.php');
419+
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/unable-to-resolve-callback-parameter-type.php');
420+
421+
require_once __DIR__ . '/../Rules/Functions/data/varying-acceptor.php';
422+
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/varying-acceptor.php');
419423
}
420424

421425
/**

tests/PHPStan/Analyser/data/bug-3158.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,48 @@ function createProxy(
2020
return $t;
2121
}
2222

23-
class AParent {}
23+
/**
24+
* @template T as object
25+
* @param \Closure(T, T):void $outmaker
26+
* @return T
27+
*/
28+
function createProxy2(
29+
\Closure $outmaker
30+
) : object {
31+
32+
}
33+
34+
class AAParent {}
35+
36+
class AParent extends AAParent {}
2437

2538
class A extends AParent {
2639
public function bar() : void {}
2740
}
2841

42+
class B extends A {
43+
44+
}
45+
2946
function (): void {
3047
$proxy = createProxy(A::class, function(AParent $o):void {});
3148
assertType(A::class, $proxy);
49+
50+
$proxy = createProxy(A::class, function($o):void {});
51+
assertType(A::class, $proxy);
52+
53+
$proxy = createProxy(A::class, function(B $o):void {});
54+
assertType(A::class, $proxy);
55+
56+
$proxy = createProxy2(function(A $a, B $o):void {});
57+
assertType(B::class, $proxy);
58+
};
59+
60+
function (): void {
61+
/** @var object $object */
62+
$object = doFoo();
63+
$objectClass = get_class($object);
64+
assertType('class-string', $objectClass);
65+
$proxy = createProxy($objectClass, function(AParent $o):void {});
66+
assertType('object', $proxy);
3267
};

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,4 +638,15 @@ public function testUksortCallback(): void
638638
]);
639639
}
640640

641+
public function testVaryingAcceptor(): void
642+
{
643+
require_once __DIR__ . '/data/varying-acceptor.php';
644+
$this->analyse([__DIR__ . '/data/varying-acceptor.php'], [
645+
[
646+
'Parameter #1 $closure of function VaryingAcceptor\bar expects callable(callable(): string): string, callable(callable(): int): string given.',
647+
17,
648+
],
649+
]);
650+
}
651+
641652
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace VaryingAcceptor;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template T
9+
*
10+
* @param callable(callable():T):T $closure
11+
* @return T
12+
*/
13+
function bar(callable $closure) { throw new \Exception(); }
14+
15+
/** @param callable(callable():int):string $callable */
16+
function testBar($callable): void {
17+
$a = bar($callable);
18+
assertType('string', $a); // can be int, but definitely not the union
19+
}

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1951,4 +1951,12 @@ public function testDoNotReportGenericReturnTypeResolvedToNever(): void
19511951
$this->analyse([__DIR__ . '/data/generic-return-type-never.php'], []);
19521952
}
19531953

1954+
public function testUnableToResolveCallbackParameterType(): void
1955+
{
1956+
$this->checkThisOnly = false;
1957+
$this->checkNullables = true;
1958+
$this->checkUnionTypes = true;
1959+
$this->analyse([__DIR__ . '/data/unable-to-resolve-callback-parameter-type.php'], []);
1960+
}
1961+
19541962
}

0 commit comments

Comments
 (0)