From 89ab39f447465967f11808e4622c3c42ae91cca7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:20:07 +0000 Subject: [PATCH] Fix phpstan/phpstan#11776: Preserve narrowed template bounds during resolution - When resolveTemplateTypes() replaces a template type with a standin of the same name and scope, preserve the narrower bound from PHPDoc intersections like (int|string)&TOperation - New regression test in tests/PHPStan/Analyser/nsrt/bug-11776.php - Root cause: ResolvedPropertyReflection called resolveTemplateTypes() which replaced TOperation of int|string with TOperation of scalar, losing the intersection narrowing from the PHPDoc type annotation --- src/Type/Generic/TemplateTypeHelper.php | 16 ++++++++++++ tests/PHPStan/Analyser/nsrt/bug-11776.php | 32 +++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11776.php diff --git a/src/Type/Generic/TemplateTypeHelper.php b/src/Type/Generic/TemplateTypeHelper.php index 6f09c29ccb2..5ea7cea18de 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -54,6 +54,22 @@ public static function resolveTemplateTypes( $callSiteVariance = $callSiteVariances->getVariance($type->getName()); if ($callSiteVariance === null || $callSiteVariance->invariant()) { + if ( + $newType instanceof TemplateType + && $newType->getName() === $type->getName() + && $newType->getScope()->equals($type->getScope()) + && !$type->getBound()->equals($newType->getBound()) + && $newType->getBound()->isSuperTypeOf($type->getBound())->yes() + ) { + return TemplateTypeFactory::create( + $newType->getScope(), + $newType->getName(), + $type->getBound(), + $newType->getVariance(), + $newType->getStrategy(), + $newType->getDefault(), + ); + } return $newType; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11776.php b/tests/PHPStan/Analyser/nsrt/bug-11776.php new file mode 100644 index 00000000000..94dff456258 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11776.php @@ -0,0 +1,32 @@ += 8.2 + +declare(strict_types=1); + +namespace Bug11776; + +use function PHPStan\Testing\assertType; + +/** + * @template TOperation of int|string + */ +interface EnumAsFilterInterface {} + +/** + * @template TOperation of scalar + */ +final readonly class ScalarableChoice +{ + /** + * @param class-string> $choiceClassName + */ + public function __construct(private string $choiceClassName) {} + + /** + * @return class-string> + */ + public function getChoiceClassName(): string + { + assertType('class-string>', $this->choiceClassName); + return $this->choiceClassName; + } +}