Skip to content

Commit

Permalink
Get closer to how type inference in TypeScript works
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ondrejmirtes committed Apr 16, 2021
1 parent c9b5c12 commit 3be90f0
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 0 deletions.
19 changes: 19 additions & 0 deletions src/Type/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeVariance;

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

$myTemplateTypes = [];
foreach ($myTypes as $type) {
if ($type instanceof TemplateType || ($type instanceof GenericClassStringType && $type->getGenericType() instanceof TemplateType)) {
$myTemplateTypes[] = $type;
continue;
}
$types = $types->union($type->inferTemplateTypes($receivedType));
}

if (!$types->isEmpty()) {
return $types;
}

if (count($myTemplateTypes) === 1) {
return $types->union($myTemplateTypes[0]->inferTemplateTypes($receivedType));
}

foreach ($myTypes as $type) {
$types = $types->union($type->inferTemplateTypes($receivedType));
}
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 @@ -10340,6 +10340,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-in-closure-bind.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/multi-assign.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-reduce-types-first.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4803.php');
}

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

namespace Bug4803;

use function PHPStan\Analyser\assertType;

/**
* @template T of object
*/
interface Proxy {}

class Foo
{

/**
* @template T of object
* @param Proxy<T>|T $proxyOrObject
* @return T
*/
public function doFoo($proxyOrObject)
{
assertType('Bug4803\Proxy<T of object (method Bug4803\Foo::doFoo(), argument)>|T of object (method Bug4803\Foo::doFoo(), argument)', $proxyOrObject);
}

/** @param Proxy<\stdClass> $proxy */
public function doBar($proxy): void
{
assertType('stdClass', $this->doFoo($proxy));
}

/** @param \stdClass $std */
public function doBaz($std): void
{
assertType('stdClass', $this->doFoo($std));
}

/** @param Proxy<\stdClass>|\stdClass $proxyOrStd */
public function doLorem($proxyOrStd): void
{
assertType('stdClass', $this->doFoo($proxyOrStd));
}

}

interface ProxyClassResolver
{
/**
* @template T of object
* @param class-string<Proxy<T>>|class-string<T> $className
* @return class-string<T>
*/
public function resolveClassName(string $className): string;
}

final class Client
{
private ProxyClassResolver $proxyClassResolver;

public function __construct(ProxyClassResolver $proxyClassResolver)
{
$this->proxyClassResolver = $proxyClassResolver;
}

/**
* @template T of object
* @param class-string<Proxy<T>>|class-string<T> $className
* @return class-string<T>
*/
public function getRealClass(string $className): string
{
assertType('class-string<Bug4803\Proxy<T of object (method Bug4803\Client::getRealClass(), argument)>>|class-string<T of object (method Bug4803\Client::getRealClass(), argument)>', $className);

$result = $this->proxyClassResolver->resolveClassName($className);
assertType('class-string<T of object (method Bug4803\Client::getRealClass(), argument)>', $result);

return $result;
}
}
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -485,4 +485,9 @@ public function testBug4795(): void
$this->analyse([__DIR__ . '/data/bug-4795.php'], []);
}

public function testBug4803(): void
{
$this->analyse([__DIR__ . '/../../Analyser/data/bug-4803.php'], []);
}

}

0 comments on commit 3be90f0

Please sign in to comment.