Skip to content

Commit 63e0762

Browse files
phpstan-botgithub-actions[bot]claude
committed
Fix phpstan/phpstan#9045: Narrowed template on an interface is ignored when generics is not specified (phpstan#5339)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0c1962d commit 63e0762

File tree

5 files changed

+106
-3
lines changed

5 files changed

+106
-3
lines changed

src/Reflection/ClassReflection.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
use PHPStan\Type\Generic\TemplateTypeVariance;
4949
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
5050
use PHPStan\Type\Generic\TypeProjectionHelper;
51+
use PHPStan\Type\MixedType;
5152
use PHPStan\Type\ObjectType;
5253
use PHPStan\Type\Type;
5354
use PHPStan\Type\TypeAlias;
@@ -237,7 +238,7 @@ public function getParentClass(): ?ClassReflection
237238
if ($this->isGeneric()) {
238239
$extendedType = TemplateTypeHelper::resolveTemplateTypes(
239240
$extendedType,
240-
$this->getPossiblyIncompleteActiveTemplateTypeMap(),
241+
$this->getActiveTemplateTypeMapForAncestorResolution(),
241242
$this->getCallSiteVarianceMap(),
242243
TemplateTypeVariance::createStatic(),
243244
);
@@ -1164,7 +1165,7 @@ public function getImmediateInterfaces(): array
11641165
if ($this->isGeneric()) {
11651166
$implementedType = TemplateTypeHelper::resolveTemplateTypes(
11661167
$implementedType,
1167-
$this->getPossiblyIncompleteActiveTemplateTypeMap(),
1168+
$this->getActiveTemplateTypeMapForAncestorResolution(),
11681169
$this->getCallSiteVarianceMap(),
11691170
TemplateTypeVariance::createStatic(),
11701171
true,
@@ -1686,6 +1687,37 @@ public function getPossiblyIncompleteActiveTemplateTypeMap(): TemplateTypeMap
16861687
return $this->resolvedTemplateTypeMap ?? $this->getTemplateTypeMap();
16871688
}
16881689

1690+
/**
1691+
* Returns a template type map for resolving ancestor type declarations (@extends, @implements).
1692+
* Like getPossiblyIncompleteActiveTemplateTypeMap(), but resolves ErrorType entries
1693+
* to their template bounds when the bound is not mixed. This ensures that when a child
1694+
* class narrows a template bound (e.g. `@template T of SpecificType`), the narrowed bound
1695+
* is propagated to ancestor declarations instead of being lost as ErrorType.
1696+
*/
1697+
private function getActiveTemplateTypeMapForAncestorResolution(): TemplateTypeMap
1698+
{
1699+
$map = $this->getPossiblyIncompleteActiveTemplateTypeMap();
1700+
$templateTypeMap = $this->getTemplateTypeMap();
1701+
1702+
return $map->map(static function (string $name, Type $type) use ($templateTypeMap): Type {
1703+
if (!$type instanceof ErrorType) {
1704+
return $type;
1705+
}
1706+
1707+
$templateType = $templateTypeMap->getType($name);
1708+
if (!$templateType instanceof TemplateType) {
1709+
return $type;
1710+
}
1711+
1712+
$bound = $templateType->getBound();
1713+
if ($bound instanceof MixedType) {
1714+
return $type;
1715+
}
1716+
1717+
return TemplateTypeHelper::resolveToDefaults($templateType);
1718+
});
1719+
}
1720+
16891721
private function getDefaultCallSiteVarianceMap(): TemplateTypeVarianceMap
16901722
{
16911723
if ($this->defaultCallSiteVarianceMap !== null) {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13204;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template TChild of object
9+
* @extends \ArrayAccess<int, TChild|null>
10+
*/
11+
interface ParentNode extends \ArrayAccess {}
12+
13+
class HelloWorld
14+
{
15+
public function sayHelloBug(object $node): void
16+
{
17+
if ($node instanceof ParentNode) {
18+
assertType('object|null', $node[0]);
19+
}
20+
}
21+
}

tests/PHPStan/Analyser/nsrt/bug-2676.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function (Wallet $wallet): void
3939
assertType('DoctrineIntersectionTypeIsSupertypeOf\Collection&iterable<Bug2676\BankAccount>', $bankAccounts);
4040

4141
foreach ($bankAccounts as $key => $bankAccount) {
42-
assertType('mixed', $key);
42+
assertType('(int|string)', $key);
4343
assertType('Bug2676\BankAccount', $bankAccount);
4444
}
4545
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug7185;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template TKey of array-key
9+
* @template TValue of object
10+
* @extends \IteratorAggregate<TKey, TValue>
11+
*/
12+
interface Collection extends \IteratorAggregate {}
13+
14+
function foo(Collection $list): void {
15+
$all = iterator_to_array($list);
16+
assertType('array<object>', $all);
17+
assertType('object|false', current($all));
18+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug9045;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
interface TranslationInterface {}
8+
interface TransportTranslationInterface extends TranslationInterface {
9+
public function getAdditionalInformation(): ?string;
10+
}
11+
12+
/**
13+
* @template T of TransportTranslationInterface
14+
* @extends TranslatableInterface<T>
15+
*/
16+
interface TransportInterface extends TranslatableInterface {}
17+
18+
/**
19+
* @template T of TranslationInterface
20+
*/
21+
interface TranslatableInterface
22+
{
23+
/** @phpstan-return T */
24+
public function getTranslation(): TranslationInterface;
25+
}
26+
27+
class Foo {
28+
public function bar(TransportInterface $transport): void {
29+
assertType('Bug9045\TransportTranslationInterface', $transport->getTranslation());
30+
$transport->getTranslation()->getAdditionalInformation();
31+
}
32+
}

0 commit comments

Comments
 (0)