diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 4955c9ca0b..3df31e38af 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-13204.php b/tests/PHPStan/Analyser/nsrt/bug-13204.php new file mode 100644 index 0000000000..0464794423 --- /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-2676.php b/tests/PHPStan/Analyser/nsrt/bug-2676.php index 30723db8a9..4daa2b5552 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-7185.php b/tests/PHPStan/Analyser/nsrt/bug-7185.php new file mode 100644 index 0000000000..dfe7b3e823 --- /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)); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9045.php b/tests/PHPStan/Analyser/nsrt/bug-9045.php new file mode 100644 index 0000000000..cce4e98f98 --- /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(); + } +}