From 897031647bc35f62fc892775e2a4e01515136a11 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:38:12 +0000 Subject: [PATCH 1/2] 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`, 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 --- src/Reflection/ClassReflection.php | 36 ++++++++++++++++++++++-- tests/PHPStan/Analyser/nsrt/bug-2676.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-9045.php | 32 +++++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-9045.php diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 4955c9ca0b8..3df31e38aff 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -48,6 +48,7 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Generic\TypeProjectionHelper; +use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeAlias; @@ -237,7 +238,7 @@ public function getParentClass(): ?ClassReflection if ($this->isGeneric()) { $extendedType = TemplateTypeHelper::resolveTemplateTypes( $extendedType, - $this->getPossiblyIncompleteActiveTemplateTypeMap(), + $this->getActiveTemplateTypeMapForAncestorResolution(), $this->getCallSiteVarianceMap(), TemplateTypeVariance::createStatic(), ); @@ -1164,7 +1165,7 @@ public function getImmediateInterfaces(): array if ($this->isGeneric()) { $implementedType = TemplateTypeHelper::resolveTemplateTypes( $implementedType, - $this->getPossiblyIncompleteActiveTemplateTypeMap(), + $this->getActiveTemplateTypeMapForAncestorResolution(), $this->getCallSiteVarianceMap(), TemplateTypeVariance::createStatic(), true, @@ -1686,6 +1687,37 @@ public function getPossiblyIncompleteActiveTemplateTypeMap(): TemplateTypeMap return $this->resolvedTemplateTypeMap ?? $this->getTemplateTypeMap(); } + /** + * Returns a template type map for resolving ancestor type declarations (@extends, @implements). + * Like getPossiblyIncompleteActiveTemplateTypeMap(), but resolves ErrorType entries + * to their template bounds when the bound is not mixed. This ensures that when a child + * class narrows a template bound (e.g. `@template T of SpecificType`), the narrowed bound + * is propagated to ancestor declarations instead of being lost as ErrorType. + */ + private function getActiveTemplateTypeMapForAncestorResolution(): TemplateTypeMap + { + $map = $this->getPossiblyIncompleteActiveTemplateTypeMap(); + $templateTypeMap = $this->getTemplateTypeMap(); + + return $map->map(static function (string $name, Type $type) use ($templateTypeMap): Type { + if (!$type instanceof ErrorType) { + return $type; + } + + $templateType = $templateTypeMap->getType($name); + if (!$templateType instanceof TemplateType) { + return $type; + } + + $bound = $templateType->getBound(); + if ($bound instanceof MixedType) { + return $type; + } + + return TemplateTypeHelper::resolveToDefaults($templateType); + }); + } + private function getDefaultCallSiteVarianceMap(): TemplateTypeVarianceMap { if ($this->defaultCallSiteVarianceMap !== null) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-2676.php b/tests/PHPStan/Analyser/nsrt/bug-2676.php index 30723db8a9d..4daa2b55525 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-2676.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2676.php @@ -39,7 +39,7 @@ function (Wallet $wallet): void assertType('DoctrineIntersectionTypeIsSupertypeOf\Collection&iterable', $bankAccounts); foreach ($bankAccounts as $key => $bankAccount) { - assertType('mixed', $key); + assertType('(int|string)', $key); assertType('Bug2676\BankAccount', $bankAccount); } }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9045.php b/tests/PHPStan/Analyser/nsrt/bug-9045.php new file mode 100644 index 00000000000..cce4e98f983 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9045.php @@ -0,0 +1,32 @@ + + */ +interface TransportInterface extends TranslatableInterface {} + +/** + * @template T of TranslationInterface + */ +interface TranslatableInterface +{ + /** @phpstan-return T */ + public function getTranslation(): TranslationInterface; +} + +class Foo { + public function bar(TransportInterface $transport): void { + assertType('Bug9045\TransportTranslationInterface', $transport->getTranslation()); + $transport->getTranslation()->getAdditionalInformation(); + } +} From f015f2465d72b77d585464199797253a98752bcc Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 30 Mar 2026 11:01:53 +0000 Subject: [PATCH 2/2] Add regression tests for phpstan/phpstan#7185 and phpstan/phpstan#13204 Both issues involve template bounds not being correctly resolved when generic types are used without explicit type parameters. These are now fixed by the getActiveTemplateTypeMapForAncestorResolution() change. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-13204.php | 21 +++++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-7185.php | 18 ++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13204.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-7185.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-13204.php b/tests/PHPStan/Analyser/nsrt/bug-13204.php new file mode 100644 index 00000000000..04647944238 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13204.php @@ -0,0 +1,21 @@ + + */ +interface ParentNode extends \ArrayAccess {} + +class HelloWorld +{ + public function sayHelloBug(object $node): void + { + if ($node instanceof ParentNode) { + assertType('object|null', $node[0]); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7185.php b/tests/PHPStan/Analyser/nsrt/bug-7185.php new file mode 100644 index 00000000000..dfe7b3e8231 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7185.php @@ -0,0 +1,18 @@ + + */ +interface Collection extends \IteratorAggregate {} + +function foo(Collection $list): void { + $all = iterator_to_array($list); + assertType('array', $all); + assertType('object|false', current($all)); +}