Skip to content

Commit

Permalink
Prevent "unable to resolve template type T" in conditional branches
Browse files Browse the repository at this point in the history
  • Loading branch information
rvanvelzen committed Apr 22, 2022
1 parent e4e9e08 commit 0834597
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 17 deletions.
49 changes: 32 additions & 17 deletions src/Rules/FunctionCallParametersCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
use PHPStan\Rules\Properties\PropertyReflectionFinder;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\ConditionalType;
use PHPStan\Type\ConditionalTypeForParameter;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Generic\TemplateType;
Expand All @@ -23,6 +25,7 @@
use PHPStan\Type\TypeUtils;
use PHPStan\Type\VerbosityLevel;
use PHPStan\Type\VoidType;
use function array_intersect_key;
use function array_key_exists;
use function count;
use function is_string;
Expand Down Expand Up @@ -329,26 +332,11 @@ public function check(
$originalParametersAcceptor = $parametersAcceptor->getOriginalParametersAcceptor();
$resolvedTypes = $parametersAcceptor->getResolvedTemplateTypeMap()->getTypes();
if (count($resolvedTypes) > 0) {
$returnTemplateTypes = [];
TypeTraverser::map($originalParametersAcceptor->getReturnType(), static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Type {
if ($type instanceof TemplateType) {
$returnTemplateTypes[$type->getName()] = true;
return $type;
}

return $traverse($type);
});
$returnTemplateTypes = self::extractTemplateTypes($originalParametersAcceptor->getReturnType());

$parameterTemplateTypes = [];
foreach ($originalParametersAcceptor->getParameters() as $parameter) {
TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$parameterTemplateTypes): Type {
if ($type instanceof TemplateType) {
$parameterTemplateTypes[$type->getName()] = true;
return $type;
}

return $traverse($type);
});
$parameterTemplateTypes += self::extractTemplateTypes($parameter->getType());
}

foreach ($resolvedTypes as $name => $type) {
Expand Down Expand Up @@ -485,4 +473,31 @@ private function processArguments(
return [$errors, $newArguments];
}

/**
* @return array<string, true>
*/
public static function extractTemplateTypes(Type $type): array
{
$templateTypes = [];
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$templateTypes): Type {
if ($type instanceof TemplateType) {
$templateTypes[$type->getName()] = true;
return $type;
}

if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
$ifTemplateTypes = self::extractTemplateTypes($type->getIf());
$elseTemplateTypes = self::extractTemplateTypes($type->getElse());

$templateTypes += array_intersect_key($ifTemplateTypes, $elseTemplateTypes);

return $type;
}

return $traverse($type);
});

return $templateTypes;
}

}
10 changes: 10 additions & 0 deletions src/Type/ConditionalType.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ public function __construct(
$this->else = $else;
}

public function getIf(): Type
{
return $this->if;
}

public function getElse(): Type
{
return $this->else;
}

public function getReferencedClasses(): array
{
return array_merge(
Expand Down
10 changes: 10 additions & 0 deletions src/Type/ConditionalTypeForParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ public function getParameterName(): string
return $this->parameterName;
}

public function getIf(): Type
{
return $this->if;
}

public function getElse(): Type
{
return $this->else;
}

public function toConditional(Type $subject): Type
{
return new ConditionalType(
Expand Down
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -523,4 +523,16 @@ public function testDiscussion7004(): void
]);
}

public function testTemplateTypeInOneBranchOfConditional(): void
{
$this->checkThisOnly = false;
$this->checkExplicitMixed = true;
$this->analyse([__DIR__ . '/data/template-type-in-one-branch-of-conditional.php'], [
[
'Parameter #1 $params of static method TemplateTypeInOneBranchOfConditional\DriverManager::getConnection() expects array{wrapperClass?: class-string<TemplateTypeInOneBranchOfConditional\Connection>}, array{wrapperClass: \'stdClass\'} given.',
27,
],
]);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);

namespace TemplateTypeInOneBranchOfConditional;

use stdClass;
use function PHPStan\Testing\assertType;

class Connection {}

class ChildConnection extends Connection {}

class DriverManager
{
/**
* @param array{wrapperClass?: class-string<T>} $params
* @phpstan-return ($params is array{wrapperClass:mixed} ? T : Connection)
* @template T of Connection
*/
public static function getConnection(array $params): Connection {
return new Connection();
}

public static function test(): void
{
assertType(Connection::class, DriverManager::getConnection([]));
assertType(ChildConnection::class, DriverManager::getConnection(['wrapperClass' => ChildConnection::class]));
assertType(Connection::class, DriverManager::getConnection(['wrapperClass' => stdClass::class]));
}
}

0 comments on commit 0834597

Please sign in to comment.