Skip to content

Commit f4c0c9a

Browse files
rvanvelzenondrejmirtes
authored andcommitted
Support intersection types in template bounds
Closes phpstan/phpstan#6649
1 parent 1654674 commit f4c0c9a

File tree

5 files changed

+91
-0
lines changed

5 files changed

+91
-0
lines changed

src/Rules/Generics/TemplateTypeCheck.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use PHPStan\Type\Generic\TemplateType;
1919
use PHPStan\Type\Generic\TemplateTypeScope;
2020
use PHPStan\Type\IntegerType;
21+
use PHPStan\Type\IntersectionType;
2122
use PHPStan\Type\MixedType;
2223
use PHPStan\Type\ObjectType;
2324
use PHPStan\Type\ObjectWithoutClassType;
@@ -107,6 +108,7 @@ public function check(
107108
&& $boundTypeClass !== ObjectType::class
108109
&& $boundTypeClass !== GenericObjectType::class
109110
&& !$boundType instanceof UnionType
111+
&& !$boundType instanceof IntersectionType
110112
&& !$boundType instanceof TemplateType
111113
) {
112114
$messages[] = RuleErrorBuilder::message(sprintf($notSupportedBoundMessage, $templateTagName, $boundType->describe(VerbosityLevel::typeOnly())))->build();
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Generic;
4+
5+
use PHPStan\Type\IntersectionType;
6+
use PHPStan\Type\Type;
7+
8+
/** @api */
9+
final class TemplateIntersectionType extends IntersectionType implements TemplateType
10+
{
11+
12+
/** @use TemplateTypeTrait<IntersectionType> */
13+
use TemplateTypeTrait;
14+
15+
public function __construct(
16+
TemplateTypeScope $scope,
17+
TemplateTypeStrategy $templateTypeStrategy,
18+
TemplateTypeVariance $templateTypeVariance,
19+
string $name,
20+
IntersectionType $bound,
21+
)
22+
{
23+
parent::__construct($bound->getTypes());
24+
25+
$this->scope = $scope;
26+
$this->strategy = $templateTypeStrategy;
27+
$this->variance = $templateTypeVariance;
28+
$this->name = $name;
29+
$this->bound = $bound;
30+
}
31+
32+
public function traverse(callable $cb): Type
33+
{
34+
$newBound = $cb($this->getBound());
35+
if ($this->getBound() !== $newBound && $newBound instanceof IntersectionType) {
36+
return new self(
37+
$this->scope,
38+
$this->strategy,
39+
$this->variance,
40+
$this->name,
41+
$newBound,
42+
);
43+
}
44+
45+
return $this;
46+
}
47+
48+
}

src/Type/Generic/TemplateTypeFactory.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Type\Constant\ConstantArrayType;
1010
use PHPStan\Type\FloatType;
1111
use PHPStan\Type\IntegerType;
12+
use PHPStan\Type\IntersectionType;
1213
use PHPStan\Type\MixedType;
1314
use PHPStan\Type\ObjectType;
1415
use PHPStan\Type\ObjectWithoutClassType;
@@ -79,6 +80,10 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou
7980
}
8081
}
8182

83+
if ($bound instanceof IntersectionType) {
84+
return new TemplateIntersectionType($scope, $strategy, $variance, $name, $bound);
85+
}
86+
8287
return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true));
8388
}
8489

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,12 @@ public function testBug6866(): void
601601
$this->assertNoErrors($errors);
602602
}
603603

604+
public function testBug6649(): void
605+
{
606+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-6649.php');
607+
$this->assertNoErrors($errors);
608+
}
609+
604610
/**
605611
* @param string[]|null $allAnalysedFiles
606612
* @return Error[]
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Bug6649;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo {}
8+
interface Bar {}
9+
10+
class FooBar extends Foo implements Bar {}
11+
12+
/**
13+
* @template TKey of Foo
14+
*/
15+
class Collection {}
16+
17+
/**
18+
* @template TKey of Foo&Bar
19+
* @extends Collection<TKey>
20+
*/
21+
class SubCollection extends Collection {
22+
/** @param TKey $key */
23+
public function __construct($key) {
24+
assertType('TKey of Bug6649\Bar&Bug6649\Foo (class Bug6649\SubCollection, argument)', $key);
25+
}
26+
27+
public static function test(): void {
28+
assertType('Bug6649\SubCollection<Bug6649\FooBar>', new SubCollection(new FooBar()));
29+
}
30+
}

0 commit comments

Comments
 (0)