Skip to content

Commit f88e210

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
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
1 parent d8f5be7 commit f88e210

3 files changed

Lines changed: 43 additions & 5 deletions

File tree

src/Type/Generic/TemplateTypeHelper.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,7 @@ public static function generalizeInferredTemplateType(TemplateType $templateType
142142
{
143143
if (!$templateType->getVariance()->covariant()) {
144144
$isArrayKey = $templateType->getBound()->describe(VerbosityLevel::precise()) === '(int|string)';
145-
if ($type->isScalar()->yes() && $isArrayKey) {
146-
$type = $type->generalize(GeneralizePrecision::templateArgument());
147-
} elseif ($type->isConstantValue()->yes() && (!$templateType->getBound()->isScalar()->yes() || $isArrayKey)) {
145+
if ($type->isConstantValue()->yes() && ($isArrayKey || !$templateType->getBound()->isScalar()->yes())) {
148146
$type = $type->generalize(GeneralizePrecision::templateArgument());
149147
}
150148
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug12601;
6+
7+
use ArrayIterator;
8+
use IteratorAggregate;
9+
use Traversable;
10+
use function PHPStan\Testing\assertType;
11+
12+
/** @implements IteratorAggregate<non-empty-string, non-empty-string> */
13+
class HelloWorld implements IteratorAggregate
14+
{
15+
/** @param array<non-empty-string, non-empty-string> $map */
16+
public function __construct(private array $map) {}
17+
18+
/** @return Traversable<non-empty-string, non-empty-string> */
19+
public function getIterator(): Traversable
20+
{
21+
$iterator = new ArrayIterator($this->map);
22+
assertType('ArrayIterator<non-empty-string, non-empty-string>', $iterator);
23+
return $iterator;
24+
}
25+
}
26+
27+
/** @implements IteratorAggregate<int<0, max>, non-empty-string> */
28+
class HelloWorld2 implements IteratorAggregate
29+
{
30+
/** @param array<int<0, max>, non-empty-string> $map */
31+
public function __construct(private array $map) {}
32+
33+
/** @return ArrayIterator<int<0, max>, non-empty-string> */
34+
public function getIterator(): Traversable
35+
{
36+
$iterator = new ArrayIterator($this->map);
37+
assertType('ArrayIterator<int<0, max>, non-empty-string>', $iterator);
38+
return $iterator;
39+
}
40+
}

tests/PHPStan/Analyser/nsrt/generics-do-not-generalize.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ function (): void {
9797
/** @var list<string> $a */
9898
$a = doFoo();
9999

100-
assertType('ArrayIterator<int, string>', new ArrayIterator($a));
100+
assertType('ArrayIterator<int<0, max>, string>', new ArrayIterator($a));
101101
};
102102

103103
/**
@@ -115,7 +115,7 @@ function (): void {
115115
/** @var list<string> $a */
116116
$a = doFoo();
117117

118-
assertType('ArrayIterator<int, string>', createArrayIterator($a));
118+
assertType('ArrayIterator<int<0, max>, string>', createArrayIterator($a));
119119
};
120120

121121
/** @template T */

0 commit comments

Comments
 (0)