Skip to content

Commit 4de1cdc

Browse files
phpstan-botstaabm
andauthored
Fix #13809: list<mixed> becomes array<int<0, max>, mixed> without array keys being modified (#4933)
Co-authored-by: staabm <120441+staabm@users.noreply.github.com>
1 parent 0b4157d commit 4de1cdc

File tree

3 files changed

+56
-1
lines changed

3 files changed

+56
-1
lines changed

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ When assigning to an array offset, NodeScopeResolver must distinguish:
264264

265265
Misusing these leads to false positives like "might not be a list" or incorrect offset-exists checks. The fix is in `NodeScopeResolver` where property/variable assignments are processed.
266266

267+
This distinction also applies in `MutatingScope::enterForeach()`. When a foreach loop iterates by reference (`foreach ($list as &$value)`), modifying `$value` changes an existing offset, not a new one. The `IntertwinedVariableByReferenceWithExpr` created for the no-key by-reference case must use `SetExistingOffsetValueTypeExpr` (not `SetOffsetValueTypeExpr`) so that `AccessoryArrayListType::setExistingOffsetValueType()` preserves the list type. Using `SetOffsetValueTypeExpr` causes `AccessoryArrayListType::setOffsetValueType()` to return `ErrorType` for non-null/non-zero offsets, destroying the list type in the intersection.
268+
267269
### ConstantArrayType and offset tracking
268270

269271
Many bugs involve `ConstantArrayType` (array shapes with known keys). Common issues:

src/Analyser/MutatingScope.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3023,7 +3023,7 @@ public function enterForeach(self $originalScope, Expr $iteratee, string $valueN
30233023
);
30243024
if ($valueByRef && $iterateeType->isArray()->yes() && $iterateeType->isConstantArray()->no()) {
30253025
$scope = $scope->assignExpression(
3026-
new IntertwinedVariableByReferenceWithExpr($valueName, $iteratee, new SetOffsetValueTypeExpr(
3026+
new IntertwinedVariableByReferenceWithExpr($valueName, $iteratee, new SetExistingOffsetValueTypeExpr(
30273027
$iteratee,
30283028
new GetIterableKeyTypeExpr($iteratee),
30293029
new Variable($valueName),
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13809;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @param list<mixed> $list
9+
*/
10+
function foo(array $list): void
11+
{
12+
foreach ($list as &$value) {
13+
$value = 'foo';
14+
}
15+
16+
assertType('list<mixed>', $list);
17+
}
18+
19+
/**
20+
* @param list<mixed> $list
21+
*/
22+
function bar(array $list): void
23+
{
24+
foreach ($list as $key => &$value) {
25+
$value = 'foo';
26+
}
27+
28+
assertType("list<'foo'>", $list);
29+
}
30+
31+
/**
32+
* @param list<string> $list
33+
*/
34+
function baz(array $list): void
35+
{
36+
foreach ($list as &$value) {
37+
$value = 'bar';
38+
}
39+
40+
assertType('list<string>', $list);
41+
}
42+
43+
/**
44+
* @param list<int> $list
45+
*/
46+
function qux(array $list): void
47+
{
48+
foreach ($list as &$value) {
49+
$value = $value + 1;
50+
}
51+
52+
assertType('list<int>', $list);
53+
}

0 commit comments

Comments
 (0)