From 811ca69a1890477caa260b0353bb52bf36b886f8 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:12:47 +0000 Subject: [PATCH] Fix union of constant string types lost when used with generics - Changed TemplateTypeHelper::generalizeInferredTemplateType() to only generalize integer types (not strings) when template bound is array-key - Previously, all scalar constant values were generalized when the bound was array-key, turning 'one'|'two' into string - Updated test expectations in bug-13144.php and native-reflection-default-values.php to reflect the more precise types - New regression test in tests/PHPStan/Analyser/nsrt/bug-8031.php Closes https://github.com/phpstan/phpstan/issues/8031 --- src/Type/Generic/TemplateTypeHelper.php | 4 +-- tests/PHPStan/Analyser/nsrt/bug-13144.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-8031.php | 32 +++++++++++++++++++ .../nsrt/native-reflection-default-values.php | 2 +- 4 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-8031.php diff --git a/src/Type/Generic/TemplateTypeHelper.php b/src/Type/Generic/TemplateTypeHelper.php index 6f09c29ccb..5933be8e5b 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -142,9 +142,9 @@ public static function generalizeInferredTemplateType(TemplateType $templateType { if (!$templateType->getVariance()->covariant()) { $isArrayKey = $templateType->getBound()->describe(VerbosityLevel::precise()) === '(int|string)'; - if ($type->isScalar()->yes() && $isArrayKey) { + if ($type->isInteger()->yes() && $isArrayKey) { $type = $type->generalize(GeneralizePrecision::templateArgument()); - } elseif ($type->isConstantValue()->yes() && (!$templateType->getBound()->isScalar()->yes() || $isArrayKey)) { + } elseif ($type->isConstantValue()->yes() && !$templateType->getBound()->isScalar()->yes()) { $type = $type->generalize(GeneralizePrecision::templateArgument()); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-13144.php b/tests/PHPStan/Analyser/nsrt/bug-13144.php index fb47f4d862..280c6fa07c 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13144.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13144.php @@ -8,7 +8,7 @@ $arr = new ArrayObject(['a' => 1, 'b' => 2]); -assertType('ArrayObject', $arr); // correctly inferred as `ArrayObject` +assertType("ArrayObject<'a'|'b', int>", $arr); $a = $arr['a']; // ok $b = $arr['b']; // ok diff --git a/tests/PHPStan/Analyser/nsrt/bug-8031.php b/tests/PHPStan/Analyser/nsrt/bug-8031.php new file mode 100644 index 0000000000..e07707109a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8031.php @@ -0,0 +1,32 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug8031; + +use function PHPStan\Testing\assertType; + +/** + * @template TKey of array-key + * @template TValue of mixed + */ +class Collection +{ + /** + * @param array $val + */ + public function __construct(protected array $val) {} +} + +/** + * @return Collection<'one'|'two', int> + */ +function test(): Collection +{ + $c = new Collection([ + 'one' => 1, + 'two' => 2, + ]); + assertType("Bug8031\Collection<'one'|'two', int>", $c); + return $c; +} diff --git a/tests/PHPStan/Analyser/nsrt/native-reflection-default-values.php b/tests/PHPStan/Analyser/nsrt/native-reflection-default-values.php index 5d0b8cdeab..86bc65ddf4 100644 --- a/tests/PHPStan/Analyser/nsrt/native-reflection-default-values.php +++ b/tests/PHPStan/Analyser/nsrt/native-reflection-default-values.php @@ -7,5 +7,5 @@ function () { assertType('ArrayObject<*NEVER*, *NEVER*>', new \ArrayObject()); assertType('ArrayObject<*NEVER*, *NEVER*>', new \ArrayObject([])); - assertType('ArrayObject', new \ArrayObject(['key' => 1])); + assertType("ArrayObject<'key', int>", new \ArrayObject(['key' => 1])); };