Skip to content

Commit 3be90f0

Browse files
committed
Get closer to how type inference in TypeScript works
See https://javascript.xgqfrms.xyz/pdfs/TypeScript%20Language%20Specification.pdf - section 3.11.7 Type Inference about union types Otherwise, if T is a union or intersection type: * First, inferences are made from S to each constituent type in T that isn't simply one of the type parameters for which inferences are being made. * If the first step produced no inferences then if T is a union type and exactly one constituent type in T is simply a type parameter for which inferences are being made, inferences are made from S to that type parameter.
1 parent c9b5c12 commit 3be90f0

File tree

4 files changed

+103
-0
lines changed

4 files changed

+103
-0
lines changed

src/Type/UnionType.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
1313
use PHPStan\TrinaryLogic;
1414
use PHPStan\Type\Constant\ConstantBooleanType;
15+
use PHPStan\Type\Generic\GenericClassStringType;
16+
use PHPStan\Type\Generic\TemplateType;
1517
use PHPStan\Type\Generic\TemplateTypeMap;
1618
use PHPStan\Type\Generic\TemplateTypeVariance;
1719

@@ -599,6 +601,23 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
599601
$myTypes = $this->types;
600602
}
601603

604+
$myTemplateTypes = [];
605+
foreach ($myTypes as $type) {
606+
if ($type instanceof TemplateType || ($type instanceof GenericClassStringType && $type->getGenericType() instanceof TemplateType)) {
607+
$myTemplateTypes[] = $type;
608+
continue;
609+
}
610+
$types = $types->union($type->inferTemplateTypes($receivedType));
611+
}
612+
613+
if (!$types->isEmpty()) {
614+
return $types;
615+
}
616+
617+
if (count($myTemplateTypes) === 1) {
618+
return $types->union($myTemplateTypes[0]->inferTemplateTypes($receivedType));
619+
}
620+
602621
foreach ($myTypes as $type) {
603622
$types = $types->union($type->inferTemplateTypes($receivedType));
604623
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10340,6 +10340,7 @@ public function dataFileAsserts(): iterable
1034010340
yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-in-closure-bind.php');
1034110341
yield from $this->gatherAssertTypes(__DIR__ . '/data/multi-assign.php');
1034210342
yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-reduce-types-first.php');
10343+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4803.php');
1034310344
}
1034410345

1034510346
/**
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace Bug4803;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
/**
8+
* @template T of object
9+
*/
10+
interface Proxy {}
11+
12+
class Foo
13+
{
14+
15+
/**
16+
* @template T of object
17+
* @param Proxy<T>|T $proxyOrObject
18+
* @return T
19+
*/
20+
public function doFoo($proxyOrObject)
21+
{
22+
assertType('Bug4803\Proxy<T of object (method Bug4803\Foo::doFoo(), argument)>|T of object (method Bug4803\Foo::doFoo(), argument)', $proxyOrObject);
23+
}
24+
25+
/** @param Proxy<\stdClass> $proxy */
26+
public function doBar($proxy): void
27+
{
28+
assertType('stdClass', $this->doFoo($proxy));
29+
}
30+
31+
/** @param \stdClass $std */
32+
public function doBaz($std): void
33+
{
34+
assertType('stdClass', $this->doFoo($std));
35+
}
36+
37+
/** @param Proxy<\stdClass>|\stdClass $proxyOrStd */
38+
public function doLorem($proxyOrStd): void
39+
{
40+
assertType('stdClass', $this->doFoo($proxyOrStd));
41+
}
42+
43+
}
44+
45+
interface ProxyClassResolver
46+
{
47+
/**
48+
* @template T of object
49+
* @param class-string<Proxy<T>>|class-string<T> $className
50+
* @return class-string<T>
51+
*/
52+
public function resolveClassName(string $className): string;
53+
}
54+
55+
final class Client
56+
{
57+
private ProxyClassResolver $proxyClassResolver;
58+
59+
public function __construct(ProxyClassResolver $proxyClassResolver)
60+
{
61+
$this->proxyClassResolver = $proxyClassResolver;
62+
}
63+
64+
/**
65+
* @template T of object
66+
* @param class-string<Proxy<T>>|class-string<T> $className
67+
* @return class-string<T>
68+
*/
69+
public function getRealClass(string $className): string
70+
{
71+
assertType('class-string<Bug4803\Proxy<T of object (method Bug4803\Client::getRealClass(), argument)>>|class-string<T of object (method Bug4803\Client::getRealClass(), argument)>', $className);
72+
73+
$result = $this->proxyClassResolver->resolveClassName($className);
74+
assertType('class-string<T of object (method Bug4803\Client::getRealClass(), argument)>', $result);
75+
76+
return $result;
77+
}
78+
}

tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,4 +485,9 @@ public function testBug4795(): void
485485
$this->analyse([__DIR__ . '/data/bug-4795.php'], []);
486486
}
487487

488+
public function testBug4803(): void
489+
{
490+
$this->analyse([__DIR__ . '/../../Analyser/data/bug-4803.php'], []);
491+
}
492+
488493
}

0 commit comments

Comments
 (0)