From f88e210eeb953a3265273c276f96829f62560088 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 23:56:03 +0000 Subject: [PATCH 1/2] Fix array key narrowing lost in template type generalization - Stop generalizing non-constant scalar subtypes (non-empty-string, int<0, max>) when inferring template types bounded by array-key - The previous logic generalized ALL scalars for array-key bounds, but only constant values need generalization - Updated existing test expectations in generics-do-not-generalize.php to reflect correct behavior - New regression test in tests/PHPStan/Analyser/nsrt/bug-12601.php --- src/Type/Generic/TemplateTypeHelper.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-12601.php | 40 +++++++++++++++++++ .../nsrt/generics-do-not-generalize.php | 4 +- 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12601.php diff --git a/src/Type/Generic/TemplateTypeHelper.php b/src/Type/Generic/TemplateTypeHelper.php index 6f09c29ccb..48ac6f2159 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -142,9 +142,7 @@ public static function generalizeInferredTemplateType(TemplateType $templateType { if (!$templateType->getVariance()->covariant()) { $isArrayKey = $templateType->getBound()->describe(VerbosityLevel::precise()) === '(int|string)'; - if ($type->isScalar()->yes() && $isArrayKey) { - $type = $type->generalize(GeneralizePrecision::templateArgument()); - } elseif ($type->isConstantValue()->yes() && (!$templateType->getBound()->isScalar()->yes() || $isArrayKey)) { + if ($type->isConstantValue()->yes() && ($isArrayKey || !$templateType->getBound()->isScalar()->yes())) { $type = $type->generalize(GeneralizePrecision::templateArgument()); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12601.php b/tests/PHPStan/Analyser/nsrt/bug-12601.php new file mode 100644 index 0000000000..b4fdcd3eb3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12601.php @@ -0,0 +1,40 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug12601; + +use ArrayIterator; +use IteratorAggregate; +use Traversable; +use function PHPStan\Testing\assertType; + +/** @implements IteratorAggregate */ +class HelloWorld implements IteratorAggregate +{ + /** @param array $map */ + public function __construct(private array $map) {} + + /** @return Traversable */ + public function getIterator(): Traversable + { + $iterator = new ArrayIterator($this->map); + assertType('ArrayIterator', $iterator); + return $iterator; + } +} + +/** @implements IteratorAggregate, non-empty-string> */ +class HelloWorld2 implements IteratorAggregate +{ + /** @param array, non-empty-string> $map */ + public function __construct(private array $map) {} + + /** @return ArrayIterator, non-empty-string> */ + public function getIterator(): Traversable + { + $iterator = new ArrayIterator($this->map); + assertType('ArrayIterator, non-empty-string>', $iterator); + return $iterator; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/generics-do-not-generalize.php b/tests/PHPStan/Analyser/nsrt/generics-do-not-generalize.php index d00b8b699a..67b1a40ee8 100644 --- a/tests/PHPStan/Analyser/nsrt/generics-do-not-generalize.php +++ b/tests/PHPStan/Analyser/nsrt/generics-do-not-generalize.php @@ -97,7 +97,7 @@ function (): void { /** @var list $a */ $a = doFoo(); - assertType('ArrayIterator', new ArrayIterator($a)); + assertType('ArrayIterator, string>', new ArrayIterator($a)); }; /** @@ -115,7 +115,7 @@ function (): void { /** @var list $a */ $a = doFoo(); - assertType('ArrayIterator', createArrayIterator($a)); + assertType('ArrayIterator, string>', createArrayIterator($a)); }; /** @template T */ From 6f58959b141498539fe69c8ce2516cabbaf5926f Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 29 Mar 2026 06:58:09 +0000 Subject: [PATCH 2/2] Add test for list key type preserved through ArrayIterator Adds a test case from phpstan.org/r/c448c867 verifying that when a list is passed to new ArrayIterator(), the inferred key type is int<0, max> (not generalized to int). Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-12601.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12601.php b/tests/PHPStan/Analyser/nsrt/bug-12601.php index b4fdcd3eb3..5b1bde9f73 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12601.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12601.php @@ -24,6 +24,20 @@ public function getIterator(): Traversable } } +class HelloWorld3 +{ + /** @var ArrayIterator */ + private ArrayIterator $a; + + /** @param list $map */ + public function __construct(private array $map) { + $a = new ArrayIterator($this->map); + assertType('ArrayIterator, string>', $a); + + $this->a = $a; + } +} + /** @implements IteratorAggregate, non-empty-string> */ class HelloWorld2 implements IteratorAggregate {