Skip to content

Commit ac7b49e

Browse files
committed
Support for union type as template type bound
1 parent dbf5eef commit ac7b49e

File tree

8 files changed

+187
-1
lines changed

8 files changed

+187
-1
lines changed

src/Rules/Generics/TemplateTypeCheck.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Type\MixedType;
1212
use PHPStan\Type\ObjectType;
1313
use PHPStan\Type\ObjectWithoutClassType;
14+
use PHPStan\Type\UnionType;
1415
use PHPStan\Type\VerbosityLevel;
1516
use function array_key_exists;
1617
use function array_map;
@@ -106,6 +107,7 @@ public function check(
106107
$boundClass === MixedType::class
107108
|| $boundClass === ObjectWithoutClassType::class
108109
|| $bound instanceof ObjectType
110+
|| $bound instanceof UnionType
109111
) {
110112
continue;
111113
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Generic;
4+
5+
use PHPStan\Type\Type;
6+
use PHPStan\Type\BenevolentUnionType;
7+
8+
final class TemplateBenevolentUnionType extends BenevolentUnionType implements TemplateType
9+
{
10+
11+
use TemplateTypeTrait;
12+
13+
/**
14+
* @param Type[] $types
15+
*/
16+
public function __construct(
17+
TemplateTypeScope $scope,
18+
TemplateTypeStrategy $templateTypeStrategy,
19+
TemplateTypeVariance $templateTypeVariance,
20+
array $types,
21+
string $name
22+
)
23+
{
24+
parent::__construct($types);
25+
26+
$this->scope = $scope;
27+
$this->strategy = $templateTypeStrategy;
28+
$this->variance = $templateTypeVariance;
29+
$this->name = $name;
30+
$this->bound = new BenevolentUnionType($types);
31+
}
32+
33+
public function toArgument(): TemplateType
34+
{
35+
return new self(
36+
$this->scope,
37+
new TemplateTypeArgumentStrategy(),
38+
$this->variance,
39+
$this->getTypes(),
40+
$this->name
41+
);
42+
}
43+
44+
/**
45+
* @param mixed[] $properties
46+
* @return Type
47+
*/
48+
public static function __set_state(array $properties): Type
49+
{
50+
return new self(
51+
$properties['scope'],
52+
$properties['strategy'],
53+
$properties['variance'],
54+
$properties['types'],
55+
$properties['name']
56+
);
57+
}
58+
59+
}

src/Type/Generic/TemplateTypeFactory.php

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

55
use PHPStan\PhpDoc\Tag\TemplateTag;
6+
use PHPStan\Type\BenevolentUnionType;
67
use PHPStan\Type\MixedType;
78
use PHPStan\Type\ObjectType;
89
use PHPStan\Type\ObjectWithoutClassType;
910
use PHPStan\Type\Type;
11+
use PHPStan\Type\UnionType;
1012

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

37+
if ($bound instanceof UnionType) {
38+
if ($boundClass === UnionType::class) {
39+
return new TemplateUnionType($scope, $strategy, $variance, $bound->getTypes(), $name);
40+
}
41+
42+
if ($boundClass === BenevolentUnionType::class) {
43+
return new TemplateBenevolentUnionType($scope, $strategy, $variance, $bound->getTypes(), $name);
44+
}
45+
}
46+
3547
return new TemplateMixedType($scope, $strategy, $variance, $name);
3648
}
3749

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Generic;
4+
5+
use PHPStan\Type\Type;
6+
use PHPStan\Type\UnionType;
7+
8+
final class TemplateUnionType extends UnionType implements TemplateType
9+
{
10+
11+
use TemplateTypeTrait;
12+
13+
/**
14+
* @param Type[] $types
15+
*/
16+
public function __construct(
17+
TemplateTypeScope $scope,
18+
TemplateTypeStrategy $templateTypeStrategy,
19+
TemplateTypeVariance $templateTypeVariance,
20+
array $types,
21+
string $name
22+
)
23+
{
24+
parent::__construct($types);
25+
26+
$this->scope = $scope;
27+
$this->strategy = $templateTypeStrategy;
28+
$this->variance = $templateTypeVariance;
29+
$this->name = $name;
30+
$this->bound = new UnionType($types);
31+
}
32+
33+
public function toArgument(): TemplateType
34+
{
35+
return new self(
36+
$this->scope,
37+
new TemplateTypeArgumentStrategy(),
38+
$this->variance,
39+
$this->getTypes(),
40+
$this->name
41+
);
42+
}
43+
44+
/**
45+
* @param mixed[] $properties
46+
* @return Type
47+
*/
48+
public static function __set_state(array $properties): Type
49+
{
50+
return new self(
51+
$properties['scope'],
52+
$properties['strategy'],
53+
$properties['variance'],
54+
$properties['types'],
55+
$properties['name']
56+
);
57+
}
58+
59+
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10907,6 +10907,12 @@ public function dataBug3321(): array
1090710907
return $this->gatherAssertTypes(__DIR__ . '/data/bug-3321.php');
1090810908
}
1090910909

10910+
public function dataBug3769(): array
10911+
{
10912+
require_once __DIR__ . '/../Rules/Generics/data/bug-3769.php';
10913+
return $this->gatherAssertTypes(__DIR__ . '/../Rules/Generics/data/bug-3769.php');
10914+
}
10915+
1091010916
/**
1091110917
* @param string $file
1091210918
* @return array<string, mixed[]>
@@ -11150,6 +11156,7 @@ private function gatherAssertTypes(string $file): array
1115011156
* @dataProvider dataBug4577
1115111157
* @dataProvider dataBug4579
1115211158
* @dataProvider dataBug3321
11159+
* @dataProvider dataBug3769
1115311160
* @param string $assertType
1115411161
* @param string $file
1115511162
* @param mixed ...$args

tests/PHPStan/Generics/TemplateTypeFactoryTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ public function dataCreate(): array
5656
new StringType(),
5757
new IntegerType(),
5858
]),
59-
new MixedType(),
59+
new UnionType([
60+
new StringType(),
61+
new IntegerType(),
62+
]),
6063
],
6164
];
6265
}

tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,10 @@ public function testRule(): void
4444
]);
4545
}
4646

47+
public function testBug3769(): void
48+
{
49+
require_once __DIR__ . '/data/bug-3769.php';
50+
$this->analyse([__DIR__ . '/data/bug-3769.php'], []);
51+
}
52+
4753
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Bug3769;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
/**
8+
* @template K of array-key
9+
* @param array<K, int> $in
10+
* @return array<K, string>
11+
*/
12+
function stringValues(array $in): array {
13+
assertType('array<K of (int|string) (function Bug3769\stringValues(), argument), int>', $in);
14+
return array_map(fn (int $int): string => (string) $int, $in);
15+
}
16+
17+
/**
18+
* @param array<int, int> $foo
19+
* @param array<string, int> $bar
20+
* @param array<int> $baz
21+
*/
22+
function foo(
23+
array $foo,
24+
array $bar,
25+
array $baz
26+
): void {
27+
assertType('array<int, string>', stringValues($foo));
28+
assertType('array<string, string>', stringValues($bar));
29+
assertType('array<string>', stringValues($baz));
30+
};
31+
32+
/**
33+
* @template T of \stdClass|\Exception
34+
* @param T $foo
35+
*/
36+
function fooUnion($foo): void {
37+
assertType('T of Exception|stdClass (function Bug3769\fooUnion(), argument)', $foo);
38+
}

0 commit comments

Comments
 (0)