Skip to content

Commit 8970316

Browse files
github-actions[bot]staabm
authored andcommitted
Fix phpstan/phpstan#9045: Narrowed template on interface ignored when generics not specified
- When a child interface narrows a template bound (e.g. `@template T of SpecificType`) and passes it to a parent via `@extends Parent<T>`, using the child without specifying generics now correctly resolves to the narrowed bound instead of the parent's wider bound - Added `getActiveTemplateTypeMapForAncestorResolution()` in ClassReflection that resolves ErrorType entries to template bounds when the bound is not mixed - Applied this resolution in both `getImmediateInterfaces()` and `getParentClass()` - Updated bug-2676 test expectation: Collection's TKey now correctly resolves to `(int|string)` (array-key bound) instead of `mixed` when not explicitly specified - New regression test in tests/PHPStan/Analyser/nsrt/bug-9045.php
1 parent 8b36ae3 commit 8970316

File tree

3 files changed

+67
-3
lines changed

3 files changed

+67
-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) {

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: 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)