Skip to content

Commit 4fae88a

Browse files
staabmphpstan-bot
authored andcommitted
Fix phpstan/phpstan#14170: list lost in iterable type after iteration
- Override getOffsetValueType() on IterableType to return the item type instead of mixed - Skip generalOffsetAccessibleType intersection for iterable types in produceArrayDimFetchAssignValueToWrite to prevent decomposing iterable into array|ArrayAccess - Update existing iterable test to reflect correct offset value type - New regression test in tests/PHPStan/Analyser/nsrt/bug-14170.php
1 parent 3222c52 commit 4fae88a

4 files changed

Lines changed: 46 additions & 1 deletion

File tree

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar
946946
if (
947947
!$offsetValueType instanceof MixedType
948948
&& !$offsetValueType->isArray()->yes()
949+
&& !($offsetValueType->isIterable()->yes() && !$offsetValueType->isObject()->yes())
949950
) {
950951
if ($offsetType !== null && $offsetType->isInteger()->yes()) {
951952
$offsetValueType = TypeCombinator::intersect($offsetValueType, StaticTypeFactory::intOffsetAccessibleType());

src/Type/IterableType.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,15 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic
195195
return TrinaryLogic::createMaybe();
196196
}
197197

198+
public function getOffsetValueType(Type $offsetType): Type
199+
{
200+
if ($this->getIterableKeyType()->isSuperTypeOf($offsetType)->no()) {
201+
return new ErrorType();
202+
}
203+
204+
return $this->itemType;
205+
}
206+
198207
public function toNumber(): Type
199208
{
200209
return new ErrorType();
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14170;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @param iterable<string, list<string>> $convert
11+
* @param-out iterable<string, list<string>> $convert
12+
*/
13+
function example3a(iterable &$convert): void
14+
{
15+
foreach ($convert as &$inner) {
16+
foreach ($inner as &$val) {
17+
$val = strtoupper($val);
18+
}
19+
}
20+
assertType('iterable<string, list<string>>', $convert);
21+
}
22+
23+
/**
24+
* @param iterable<string, list<string>> $convert
25+
* @param-out iterable<string, list<string>> $convert
26+
*/
27+
function example3b(iterable &$convert): void
28+
{
29+
foreach ($convert as $outerKey => $inner) {
30+
foreach ($inner as $key => $val) {
31+
$convert[$outerKey][$key] = strtoupper($val);
32+
}
33+
}
34+
assertType('iterable<string, list<string>>', $convert);
35+
}

tests/PHPStan/Analyser/nsrt/iterable.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public function doFoo(
9595
assertType('mixed', $iterableWithIterableTypehint[0]);
9696
assertType('mixed', $mixed);
9797
assertType('iterable<Iterables\Bar>', $iterableWithConcreteTypehint);
98-
assertType('mixed', $iterableWithConcreteTypehint[0]);
98+
assertType('Iterables\Bar', $iterableWithConcreteTypehint[0]);
9999
assertType('Iterables\Bar', $bar);
100100
assertType('iterable', $this->doBar());
101101
assertType('iterable<Iterables\Baz>', $this->doBaz());

0 commit comments

Comments
 (0)