Skip to content

Commit c1924d6

Browse files
Fix on constant array
1 parent a5f0c72 commit c1924d6

2 files changed

Lines changed: 112 additions & 7 deletions

File tree

src/Analyser/NodeScopeResolver.php

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,7 +1352,7 @@ public function processStmtNode(
13521352
&& ($stmt->keyVar !== null || $byRefWithoutKey)
13531353
&& (!$hasExpr->no() || !$stmt->expr instanceof Variable)
13541354
&& $exprType->isArray()->yes()
1355-
&& $exprType->isConstantArray()->no()
1355+
&& ($exprType->isConstantArray()->no() || $stmt->byRef)
13561356
) {
13571357
$nativeExprType = $scope->getNativeType($stmt->expr);
13581358
$arrayDimFetchLoopType = $exprType->getIterableValueType();
@@ -1372,17 +1372,13 @@ public function processStmtNode(
13721372
$keyLoopTypes = [];
13731373
foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
13741374
$dimFetchType = $scopeWithIterableValueType->getType($arrayExprDimFetch);
1375-
// Condition-based narrowings like `is_string($type)` apply to the value
1376-
// variable but not automatically to the array dim fetch, even though the
1377-
// two describe the same element for a given iteration. If the value var
1378-
// hasn't been reassigned (OriginalForeachValueExpr still tracked) we use
1379-
// the narrowed value-var type in place of the broader dim fetch type so
1380-
// the loop's final array rewrite below picks up the sharper element type.
13811375
if ($originalValueExpr !== null && $scopeWithIterableValueType->hasExpressionType($originalValueExpr)->yes()) {
13821376
$valueVarType = $scopeWithIterableValueType->getType($stmt->valueVar);
13831377
if ($dimFetchType->isSuperTypeOf($valueVarType)->yes()) {
13841378
$dimFetchType = $valueVarType;
13851379
}
1380+
} elseif ($stmt->byRef && $originalValueExpr !== null) {
1381+
$dimFetchType = $scopeWithIterableValueType->getType($stmt->valueVar);
13861382
}
13871383
$arrayDimFetchLoopTypes[] = $dimFetchType;
13881384
$keyLoopTypes[] = $scopeWithIterableValueType->getType($stmt->keyVar);
@@ -1400,6 +1396,8 @@ public function processStmtNode(
14001396
if ($dimFetchNativeType->isSuperTypeOf($valueVarNativeType)->yes()) {
14011397
$dimFetchNativeType = $valueVarNativeType;
14021398
}
1399+
} elseif ($stmt->byRef && $originalValueExpr !== null) {
1400+
$dimFetchNativeType = $scopeWithIterableValueType->getNativeType($stmt->valueVar);
14031401
}
14041402
$arrayDimFetchLoopNativeTypes[] = $dimFetchNativeType;
14051403
$keyLoopNativeTypes[] = $scopeWithIterableValueType->getType($stmt->keyVar);
@@ -1432,6 +1430,10 @@ public function processStmtNode(
14321430
return $traverse($type);
14331431
}
14341432

1433+
if ($type->isConstantArray()->yes() && $valueTypeChanged && !$keyTypeChanged) {
1434+
return $type->traverse(static fn () => $arrayDimFetchLoopType);
1435+
}
1436+
14351437
if (!$type instanceof ArrayType) {
14361438
return $type;
14371439
}
@@ -1446,6 +1448,10 @@ public function processStmtNode(
14461448
return $traverse($type);
14471449
}
14481450

1451+
if ($type->isConstantArray()->yes() && $valueTypeChanged && !$keyTypeChanged) {
1452+
return $type->traverse(static fn () => $arrayDimFetchLoopNativeType);
1453+
}
1454+
14491455
if (!$type instanceof ArrayType) {
14501456
return $type;
14511457
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
namespace Bug1311ConstantArray;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
/**
10+
* @var array<int, string>
11+
*/
12+
private $list = [];
13+
14+
public function convertListByRefWithoutKey(): void
15+
{
16+
$temp = [1, 2, 3];
17+
18+
foreach ($temp as &$item) {
19+
$item = (string) $item;
20+
}
21+
22+
assertType("array{'1'|'2'|'3', '1'|'2'|'3', '1'|'2'|'3'}", $temp);
23+
24+
$this->list = $temp;
25+
}
26+
27+
public function convertListByRefWithKey(): void
28+
{
29+
$temp = [1, 2, 3];
30+
31+
foreach ($temp as $k => &$item) {
32+
$item = (string) $item;
33+
}
34+
35+
assertType("array{'1'|'2'|'3', '1'|'2'|'3', '1'|'2'|'3'}", $temp);
36+
37+
$this->list = $temp;
38+
}
39+
40+
public function byRefConstantArrayConditional(): void
41+
{
42+
$temp = [1, 2, 3];
43+
44+
foreach ($temp as &$item) {
45+
if (rand(0, 1)) {
46+
$item = (string) $item;
47+
}
48+
}
49+
50+
assertType("array{1|2|3|'1'|'2'|'3', 1|2|3|'1'|'2'|'3', 1|2|3|'1'|'2'|'3'}", $temp);
51+
}
52+
53+
public function byRefConstantArrayWithBreak(): void
54+
{
55+
$temp = [1, 2, 3];
56+
57+
foreach ($temp as &$item) {
58+
$item = (string) $item;
59+
if (rand(0, 1)) {
60+
break;
61+
}
62+
}
63+
64+
assertType('array{1, 2, 3}', $temp);
65+
}
66+
67+
public function byRefConstantArrayIntval(): void
68+
{
69+
$temp = ['a', 'b', 'c'];
70+
71+
foreach ($temp as &$item) {
72+
$item = strlen($item);
73+
}
74+
75+
assertType('array{1, 1, 1}', $temp);
76+
}
77+
78+
public function byRefConstantArrayStringKeys(): void
79+
{
80+
$temp = ['x' => 1, 'y' => 2];
81+
82+
foreach ($temp as &$v) {
83+
$v = (string) $v;
84+
}
85+
86+
assertType("array{x: '1'|'2', y: '1'|'2'}", $temp);
87+
}
88+
89+
public function byRefConstantArrayNoOverwrite(): void
90+
{
91+
$temp = [1, 2, 3];
92+
93+
foreach ($temp as &$item) {
94+
echo $item;
95+
}
96+
97+
assertType('array{1, 2, 3}', $temp);
98+
}
99+
}

0 commit comments

Comments
 (0)