Skip to content

Commit

Permalink
Support for union type as template type bound
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Feb 22, 2021
1 parent dbf5eef commit ac7b49e
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/Rules/Generics/TemplateTypeCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function array_key_exists;
use function array_map;
Expand Down Expand Up @@ -106,6 +107,7 @@ public function check(
$boundClass === MixedType::class
|| $boundClass === ObjectWithoutClassType::class
|| $bound instanceof ObjectType
|| $bound instanceof UnionType
) {
continue;
}
Expand Down
59 changes: 59 additions & 0 deletions src/Type/Generic/TemplateBenevolentUnionType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Generic;

use PHPStan\Type\Type;
use PHPStan\Type\BenevolentUnionType;

final class TemplateBenevolentUnionType extends BenevolentUnionType implements TemplateType
{

use TemplateTypeTrait;

/**
* @param Type[] $types
*/
public function __construct(
TemplateTypeScope $scope,
TemplateTypeStrategy $templateTypeStrategy,
TemplateTypeVariance $templateTypeVariance,
array $types,
string $name
)
{
parent::__construct($types);

$this->scope = $scope;
$this->strategy = $templateTypeStrategy;
$this->variance = $templateTypeVariance;
$this->name = $name;
$this->bound = new BenevolentUnionType($types);
}

public function toArgument(): TemplateType
{
return new self(
$this->scope,
new TemplateTypeArgumentStrategy(),
$this->variance,
$this->getTypes(),
$this->name
);
}

/**
* @param mixed[] $properties
* @return Type
*/
public static function __set_state(array $properties): Type
{
return new self(
$properties['scope'],
$properties['strategy'],
$properties['variance'],
$properties['types'],
$properties['name']
);
}

}
12 changes: 12 additions & 0 deletions src/Type/Generic/TemplateTypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
namespace PHPStan\Type\Generic;

use PHPStan\PhpDoc\Tag\TemplateTag;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;

final class TemplateTypeFactory
{
Expand All @@ -32,6 +34,16 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou
return new TemplateMixedType($scope, $strategy, $variance, $name);
}

if ($bound instanceof UnionType) {
if ($boundClass === UnionType::class) {
return new TemplateUnionType($scope, $strategy, $variance, $bound->getTypes(), $name);
}

if ($boundClass === BenevolentUnionType::class) {
return new TemplateBenevolentUnionType($scope, $strategy, $variance, $bound->getTypes(), $name);
}
}

return new TemplateMixedType($scope, $strategy, $variance, $name);
}

Expand Down
59 changes: 59 additions & 0 deletions src/Type/Generic/TemplateUnionType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Generic;

use PHPStan\Type\Type;
use PHPStan\Type\UnionType;

final class TemplateUnionType extends UnionType implements TemplateType
{

use TemplateTypeTrait;

/**
* @param Type[] $types
*/
public function __construct(
TemplateTypeScope $scope,
TemplateTypeStrategy $templateTypeStrategy,
TemplateTypeVariance $templateTypeVariance,
array $types,
string $name
)
{
parent::__construct($types);

$this->scope = $scope;
$this->strategy = $templateTypeStrategy;
$this->variance = $templateTypeVariance;
$this->name = $name;
$this->bound = new UnionType($types);
}

public function toArgument(): TemplateType
{
return new self(
$this->scope,
new TemplateTypeArgumentStrategy(),
$this->variance,
$this->getTypes(),
$this->name
);
}

/**
* @param mixed[] $properties
* @return Type
*/
public static function __set_state(array $properties): Type
{
return new self(
$properties['scope'],
$properties['strategy'],
$properties['variance'],
$properties['types'],
$properties['name']
);
}

}
7 changes: 7 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10907,6 +10907,12 @@ public function dataBug3321(): array
return $this->gatherAssertTypes(__DIR__ . '/data/bug-3321.php');
}

public function dataBug3769(): array
{
require_once __DIR__ . '/../Rules/Generics/data/bug-3769.php';
return $this->gatherAssertTypes(__DIR__ . '/../Rules/Generics/data/bug-3769.php');
}

/**
* @param string $file
* @return array<string, mixed[]>
Expand Down Expand Up @@ -11150,6 +11156,7 @@ private function gatherAssertTypes(string $file): array
* @dataProvider dataBug4577
* @dataProvider dataBug4579
* @dataProvider dataBug3321
* @dataProvider dataBug3769
* @param string $assertType
* @param string $file
* @param mixed ...$args
Expand Down
5 changes: 4 additions & 1 deletion tests/PHPStan/Generics/TemplateTypeFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ public function dataCreate(): array
new StringType(),
new IntegerType(),
]),
new MixedType(),
new UnionType([
new StringType(),
new IntegerType(),
]),
],
];
}
Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,10 @@ public function testRule(): void
]);
}

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

}
38 changes: 38 additions & 0 deletions tests/PHPStan/Rules/Generics/data/bug-3769.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Bug3769;

use function PHPStan\Analyser\assertType;

/**
* @template K of array-key
* @param array<K, int> $in
* @return array<K, string>
*/
function stringValues(array $in): array {
assertType('array<K of (int|string) (function Bug3769\stringValues(), argument), int>', $in);
return array_map(fn (int $int): string => (string) $int, $in);
}

/**
* @param array<int, int> $foo
* @param array<string, int> $bar
* @param array<int> $baz
*/
function foo(
array $foo,
array $bar,
array $baz
): void {
assertType('array<int, string>', stringValues($foo));
assertType('array<string, string>', stringValues($bar));
assertType('array<string>', stringValues($baz));
};

/**
* @template T of \stdClass|\Exception
* @param T $foo
*/
function fooUnion($foo): void {
assertType('T of Exception|stdClass (function Bug3769\fooUnion(), argument)', $foo);
}

0 comments on commit ac7b49e

Please sign in to comment.