Skip to content

Commit b6d0c87

Browse files
committed
Helper PHPDoc type: template-type (calling Type::getTemplateType() method)
1 parent b647037 commit b6d0c87

File tree

4 files changed

+213
-0
lines changed

4 files changed

+213
-0
lines changed

Diff for: src/PhpDoc/TypeNodeResolver.php

+14
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
use PHPStan\Type\FloatType;
6464
use PHPStan\Type\Generic\GenericClassStringType;
6565
use PHPStan\Type\Generic\GenericObjectType;
66+
use PHPStan\Type\Helper\GetTemplateTypeType;
6667
use PHPStan\Type\IntegerRangeType;
6768
use PHPStan\Type\IntegerType;
6869
use PHPStan\Type\IntersectionType;
@@ -670,6 +671,19 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
670671
if (count($genericTypes) === 1) {
671672
return TypeUtils::toBenevolentUnion($genericTypes[0]);
672673
}
674+
return new ErrorType();
675+
} elseif ($mainTypeName === 'template-type') {
676+
if (count($genericTypes) === 3) {
677+
$result = [];
678+
foreach ($genericTypes[1]->getObjectClassNames() as $ancestorClassName) {
679+
foreach ($genericTypes[2]->getConstantStrings() as $templateTypeName) {
680+
$result[] = new GetTemplateTypeType($genericTypes[0], $ancestorClassName, $templateTypeName->getValue());
681+
}
682+
}
683+
684+
return TypeCombinator::union(...$result);
685+
}
686+
673687
return new ErrorType();
674688
}
675689

Diff for: src/Type/Helper/GetTemplateTypeType.php

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Helper;
4+
5+
use PHPStan\Type\CompoundType;
6+
use PHPStan\Type\Generic\TemplateTypeVariance;
7+
use PHPStan\Type\LateResolvableType;
8+
use PHPStan\Type\Traits\LateResolvableTypeTrait;
9+
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
10+
use PHPStan\Type\Type;
11+
use PHPStan\Type\TypeUtils;
12+
use PHPStan\Type\VerbosityLevel;
13+
use function sprintf;
14+
15+
/** @api */
16+
final class GetTemplateTypeType implements CompoundType, LateResolvableType
17+
{
18+
19+
use LateResolvableTypeTrait;
20+
use NonGeneralizableTypeTrait;
21+
22+
public function __construct(private Type $type, private string $ancestorClassName, private string $templateTypeName)
23+
{
24+
}
25+
26+
public function getReferencedClasses(): array
27+
{
28+
return $this->type->getReferencedClasses();
29+
}
30+
31+
public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
32+
{
33+
return $this->type->getReferencedTemplateTypes($positionVariance);
34+
}
35+
36+
public function equals(Type $type): bool
37+
{
38+
return $type instanceof self
39+
&& $this->type->equals($type->type);
40+
}
41+
42+
public function describe(VerbosityLevel $level): string
43+
{
44+
return sprintf('template-type<%s, %s, %s>', $this->type->describe($level), $this->ancestorClassName, $this->templateTypeName);
45+
}
46+
47+
public function isResolvable(): bool
48+
{
49+
return !TypeUtils::containsTemplateType($this->type);
50+
}
51+
52+
protected function getResult(): Type
53+
{
54+
return $this->type->getTemplateType($this->ancestorClassName, $this->templateTypeName);
55+
}
56+
57+
/**
58+
* @param callable(Type): Type $cb
59+
*/
60+
public function traverse(callable $cb): Type
61+
{
62+
$type = $cb($this->type);
63+
64+
if ($this->type === $type) {
65+
return $this;
66+
}
67+
68+
return new self($type, $this->ancestorClassName, $this->templateTypeName);
69+
}
70+
71+
/**
72+
* @param mixed[] $properties
73+
*/
74+
public static function __set_state(array $properties): Type
75+
{
76+
return new self(
77+
$properties['type'],
78+
$properties['ancestorClassName'],
79+
$properties['templateTypeName'],
80+
);
81+
}
82+
83+
}

Diff for: tests/PHPStan/Analyser/AnalyserIntegrationTest.php

+6
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,12 @@ public function testBug5091(): void
11761176
$this->assertNoErrors($errors);
11771177
}
11781178

1179+
public function testDiscussion9053(): void
1180+
{
1181+
$errors = $this->runAnalyse(__DIR__ . '/data/discussion-9053.php');
1182+
$this->assertNoErrors($errors);
1183+
}
1184+
11791185
/**
11801186
* @param string[]|null $allAnalysedFiles
11811187
* @return Error[]

Diff for: tests/PHPStan/Analyser/data/discussion-9053.php

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
namespace Discussion9053;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template-covariant TChild of ChildInterface
9+
*/
10+
interface ModelInterface {
11+
/**
12+
* @return TChild[]
13+
*/
14+
public function getChildren(): array;
15+
}
16+
17+
/**
18+
* @implements ModelInterface<Child>
19+
*/
20+
class Model implements ModelInterface
21+
{
22+
/**
23+
* @var Child[]
24+
*/
25+
public array $children;
26+
27+
public function getChildren(): array
28+
{
29+
return $this->children;
30+
}
31+
}
32+
33+
/**
34+
* @template-covariant T of ModelInterface
35+
*/
36+
interface ChildInterface {
37+
/**
38+
* @return T
39+
*/
40+
public function getModel(): ModelInterface;
41+
}
42+
43+
44+
/**
45+
* @implements ChildInterface<Model>
46+
*/
47+
class Child implements ChildInterface
48+
{
49+
public function __construct(private Model $model)
50+
{
51+
}
52+
53+
public function getModel(): Model
54+
{
55+
return $this->model;
56+
}
57+
}
58+
59+
/**
60+
* @template-covariant T of ModelInterface
61+
*/
62+
class Helper
63+
{
64+
/**
65+
* @param T $model
66+
*/
67+
public function __construct(private ModelInterface $model)
68+
{}
69+
70+
/**
71+
* @return template-type<T, ModelInterface, 'TChild'>
72+
*/
73+
public function getFirstChildren(): ChildInterface
74+
{
75+
$firstChildren = $this->model->getChildren()[0] ?? null;
76+
77+
if (!$firstChildren) {
78+
throw new \RuntimeException('No first child found.');
79+
}
80+
81+
return $firstChildren;
82+
}
83+
}
84+
85+
class Other {
86+
/**
87+
* @template TChild of ChildInterface
88+
* @template TModel of ModelInterface<TChild>
89+
* @param Helper<TModel> $helper
90+
* @return TChild
91+
*/
92+
public function getFirstChildren(Helper $helper): ChildInterface {
93+
$child = $helper->getFirstChildren();
94+
assertType('TChild of Discussion9053\ChildInterface (method Discussion9053\Other::getFirstChildren(), argument)', $child);
95+
96+
return $child;
97+
}
98+
}
99+
100+
function (): void {
101+
$model = new Model();
102+
$helper = new Helper($model);
103+
assertType('Discussion9053\Helper<Discussion9053\Model>', $helper);
104+
$child = $helper->getFirstChildren();
105+
assertType('Discussion9053\Child', $child);
106+
107+
$other = new Other();
108+
$child2 = $other->getFirstChildren($helper);
109+
assertType('Discussion9053\Child', $child2);
110+
};

0 commit comments

Comments
 (0)