Skip to content

Commit 7738398

Browse files
committed
Understand always-overwritten arrays in foreach
1 parent b6a2e0f commit 7738398

File tree

8 files changed

+317
-5
lines changed

8 files changed

+317
-5
lines changed

src/Analyser/MutatingScope.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
3535
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
3636
use PHPStan\Node\Expr\NativeTypeExpr;
37+
use PHPStan\Node\Expr\OriginalForeachKeyExpr;
3738
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
3839
use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr;
3940
use PHPStan\Node\Expr\PropertyInitializationExpr;
@@ -3949,13 +3950,19 @@ public function enterForeachKey(self $originalScope, Expr $iteratee, string $key
39493950
{
39503951
$iterateeType = $originalScope->getType($iteratee);
39513952
$nativeIterateeType = $originalScope->getNativeType($iteratee);
3953+
3954+
$keyType = $originalScope->getIterableKeyType($iterateeType);
3955+
$nativeKeyType = $originalScope->getIterableKeyType($nativeIterateeType);
3956+
39523957
$scope = $this->assignVariable(
39533958
$keyName,
3954-
$originalScope->getIterableKeyType($iterateeType),
3955-
$originalScope->getIterableKeyType($nativeIterateeType),
3959+
$keyType,
3960+
$nativeKeyType,
39563961
TrinaryLogic::createYes(),
39573962
);
39583963

3964+
$originalForeachKeyExpr = new OriginalForeachKeyExpr($keyName);
3965+
$scope = $scope->assignExpression($originalForeachKeyExpr, $keyType, $nativeKeyType);
39593966
if ($iterateeType->isArray()->yes()) {
39603967
$scope = $scope->assignExpression(
39613968
new Expr\ArrayDimFetch($iteratee, new Variable($keyName)),
@@ -4139,6 +4146,10 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp
41394146
unset($scope->expressionTypes[$parameterOriginalValueExprString]);
41404147
unset($scope->nativeExpressionTypes[$parameterOriginalValueExprString]);
41414148

4149+
$originalForeachKeyExpr = $this->getNodeKey(new OriginalForeachKeyExpr($variableName));
4150+
unset($scope->expressionTypes[$originalForeachKeyExpr]);
4151+
unset($scope->nativeExpressionTypes[$originalForeachKeyExpr]);
4152+
41424153
return $scope;
41434154
}
41444155

src/Analyser/NodeScopeResolver.php

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
9292
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
9393
use PHPStan\Node\Expr\NativeTypeExpr;
94+
use PHPStan\Node\Expr\OriginalForeachKeyExpr;
9495
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
9596
use PHPStan\Node\Expr\PropertyInitializationExpr;
9697
use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
@@ -1247,14 +1248,94 @@ private function processStmtNode(
12471248
$bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt, $nodeCallback);
12481249
$finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
12491250
$finalScope = $finalScopeResult->getScope();
1251+
$scopesWithIterableValueType = [];
1252+
1253+
$originalKeyVarExpr = null;
1254+
$continueExitPointHasUnoriginalKeyType = false;
1255+
if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) {
1256+
$originalKeyVarExpr = new OriginalForeachKeyExpr($stmt->keyVar->name);
1257+
if ($finalScope->hasExpressionType($originalKeyVarExpr)->yes()) {
1258+
$scopesWithIterableValueType[] = $finalScope;
1259+
} else {
1260+
$continueExitPointHasUnoriginalKeyType = true;
1261+
}
1262+
}
1263+
12501264
foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1251-
$finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
1265+
$continueScope = $continueExitPoint->getScope();
1266+
$finalScope = $continueScope->mergeWith($finalScope);
1267+
if ($originalKeyVarExpr === null || !$continueScope->hasExpressionType($originalKeyVarExpr)->yes()) {
1268+
$continueExitPointHasUnoriginalKeyType = true;
1269+
continue;
1270+
}
1271+
$scopesWithIterableValueType[] = $continueScope;
12521272
}
1253-
foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1273+
$breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class);
1274+
foreach ($breakExitPoints as $breakExitPoint) {
12541275
$finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
12551276
}
12561277

12571278
$exprType = $scope->getType($stmt->expr);
1279+
$hasExpr = $scope->hasExpressionType($stmt->expr);
1280+
if (
1281+
count($breakExitPoints) === 0
1282+
&& count($scopesWithIterableValueType) > 0
1283+
&& !$continueExitPointHasUnoriginalKeyType
1284+
&& $stmt->keyVar !== null
1285+
&& $exprType->isArray()->yes()
1286+
&& $exprType->isConstantArray()->no()
1287+
&& !$hasExpr->no()
1288+
) {
1289+
$arrayExprDimFetch = new ArrayDimFetch($stmt->expr, $stmt->keyVar);
1290+
$arrayDimFetchLoopTypes = [];
1291+
foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
1292+
$arrayDimFetchLoopTypes[] = $scopeWithIterableValueType->getType($arrayExprDimFetch);
1293+
}
1294+
1295+
$arrayDimFetchLoopType = TypeCombinator::union(...$arrayDimFetchLoopTypes);
1296+
1297+
$arrayDimFetchLoopNativeTypes = [];
1298+
foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
1299+
$arrayDimFetchLoopNativeTypes[] = $scopeWithIterableValueType->getNativeType($arrayExprDimFetch);
1300+
}
1301+
1302+
$arrayDimFetchLoopNativeType = TypeCombinator::union(...$arrayDimFetchLoopNativeTypes);
1303+
1304+
if (!$arrayDimFetchLoopType->equals($exprType->getIterableValueType())) {
1305+
$newExprType = TypeTraverser::map($exprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopType): Type {
1306+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
1307+
return $traverse($type);
1308+
}
1309+
1310+
if (!$type instanceof ArrayType) {
1311+
return $type;
1312+
}
1313+
1314+
return new ArrayType($type->getKeyType(), $arrayDimFetchLoopType);
1315+
});
1316+
$newExprNativeType = TypeTraverser::map($scope->getNativeType($stmt->expr), static function (Type $type, callable $traverse) use ($arrayDimFetchLoopNativeType): Type {
1317+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
1318+
return $traverse($type);
1319+
}
1320+
1321+
if (!$type instanceof ArrayType) {
1322+
return $type;
1323+
}
1324+
1325+
return new ArrayType($type->getKeyType(), $arrayDimFetchLoopNativeType);
1326+
});
1327+
1328+
if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
1329+
$finalScope = $finalScope->assignVariable(
1330+
$stmt->expr->name,
1331+
$newExprType,
1332+
$newExprNativeType,
1333+
$hasExpr,
1334+
);
1335+
}
1336+
}
1337+
}
1338+
12581339
$isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce();
12591340
if ($exprType->isIterable()->no() || $isIterableAtLeastOnce->maybe()) {
12601341
$finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr(
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Node\Expr;
4+
5+
use Override;
6+
use PhpParser\Node\Expr;
7+
use PHPStan\Node\VirtualNode;
8+
9+
final class OriginalForeachKeyExpr extends Expr implements VirtualNode
10+
{
11+
12+
public function __construct(private string $variableName)
13+
{
14+
parent::__construct([]);
15+
}
16+
17+
public function getVariableName(): string
18+
{
19+
return $this->variableName;
20+
}
21+
22+
#[Override]
23+
public function getType(): string
24+
{
25+
return 'PHPStan_Node_OriginalForeachKeyExpr';
26+
}
27+
28+
/**
29+
* @return string[]
30+
*/
31+
#[Override]
32+
public function getSubNodeNames(): array
33+
{
34+
return [];
35+
}
36+
37+
}

src/Node/Printer/Printer.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
1212
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
1313
use PHPStan\Node\Expr\NativeTypeExpr;
14+
use PHPStan\Node\Expr\OriginalForeachKeyExpr;
1415
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
1516
use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr;
1617
use PHPStan\Node\Expr\PropertyInitializationExpr;
@@ -99,6 +100,11 @@ protected function pPHPStan_Node_ParameterVariableOriginalValueExpr(ParameterVar
99100
return sprintf('__phpstanParameterVariableOriginalValue(%s)', $expr->getVariableName());
100101
}
101102

103+
protected function pPHPStan_Node_OriginalForeachKeyExpr(OriginalForeachKeyExpr $expr): string // phpcs:ignore
104+
{
105+
return sprintf('__phpstanOriginalForeachKey(%s)', $expr->getVariableName());
106+
}
107+
102108
protected function pPHPStan_Node_IssetExpr(IssetExpr $expr): string // phpcs:ignore
103109
{
104110
return sprintf('__phpstanIssetExpr(%s)', $this->p($expr->getExpr()));

tests/PHPStan/Analyser/nsrt/bug-12274.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function getItems(array $items): array
1515
$items[$index] = 1;
1616
}
1717

18-
assertType('non-empty-list<int>', $items);
18+
assertType('non-empty-list<1>', $items);
1919
return $items;
2020
}
2121

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13730;
4+
5+
use function PHPStan\dumpType;
6+
use function PHPStan\Testing\assertType;
7+
8+
class HelloWorld
9+
{
10+
/**
11+
* @param array<string|null> $arr
12+
* @return array<string>
13+
*/
14+
public function sayHello(array $arr): array
15+
{
16+
foreach($arr as $k => $v) {
17+
$arr[$k] = $v ?? '';
18+
}
19+
20+
assertType('array<string>', $arr);
21+
22+
return $arr;
23+
}
24+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Bug2273;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo {
8+
/**
9+
* @param string[] $x
10+
*/
11+
public function doFoo(array $x): void
12+
{
13+
foreach ($x as $k => $v) {
14+
$x[$k] = \realpath($v);
15+
if ($x[$k] === false) {
16+
throw new \Exception();
17+
}
18+
}
19+
20+
assertType('array<non-empty-string>', $x);
21+
}
22+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
namespace OverwrittenArrays;
4+
5+
use function PHPStan\Testing\assertType;
6+
use function rad2deg;
7+
8+
class Foo
9+
{
10+
11+
/**
12+
* @param array<int, string> $a
13+
*/
14+
public function doFoo(array $a): void
15+
{
16+
foreach ($a as $k => $v) {
17+
$a[$k] = 1;
18+
}
19+
20+
assertType('array<int, 1>', $a);
21+
}
22+
23+
/**
24+
* @param array<int, string> $a
25+
*/
26+
public function doFoo2(array $a): void
27+
{
28+
foreach ($a as $k => $v) {
29+
if (rand(0, 1)) {
30+
$a[$k] = 2;
31+
continue;
32+
}
33+
$a[$k] = 1;
34+
}
35+
36+
assertType('array<int, 1|2>', $a);
37+
}
38+
39+
/**
40+
* @param array<int, string> $a
41+
*/
42+
public function doFoo3(array $a): void
43+
{
44+
foreach ($a as $k => $v) {
45+
if (rand(0, 1)) {
46+
break;
47+
}
48+
if (rand(0, 1)) {
49+
$a[$k] = 2;
50+
continue;
51+
}
52+
$a[$k] = 1;
53+
}
54+
55+
assertType('array<int, 1|2|string>', $a);
56+
}
57+
58+
/**
59+
* @param array<int, string> $a
60+
*/
61+
public function doFoo4(array $a): void
62+
{
63+
foreach ($a as $k => $v) {
64+
$k++;
65+
$a[$k] = 1;
66+
}
67+
68+
assertType('array<int, 1|string>', $a);
69+
}
70+
71+
/**
72+
* @param array<int, string> $a
73+
*/
74+
public function doFoo5(array $a): void
75+
{
76+
foreach ($a as $k => $v) {
77+
if (rand(0, 1)) {
78+
$k++;
79+
$a[$k] = 2;
80+
continue;
81+
}
82+
$a[$k] = 1;
83+
}
84+
85+
assertType('array<int, 1|2|string>', $a);
86+
}
87+
88+
/**
89+
* @param array<int, string> $a
90+
*/
91+
public function doFoo6(array $a): void
92+
{
93+
foreach ($a as $k => $v) {
94+
if (rand(0, 1)) {
95+
$a[$k] = 2;
96+
continue;
97+
}
98+
$k++;
99+
$a[$k] = 1;
100+
}
101+
102+
assertType('array<int, 1|2|string>', $a);
103+
}
104+
105+
/**
106+
* @param array<int, string> $a
107+
*/
108+
public function doFoo7(array $a): void
109+
{
110+
foreach ($a as &$v) {
111+
$v = 1;
112+
}
113+
114+
assertType('array<int, 1>', $a);
115+
}
116+
117+
/**
118+
* @param array<int, string> $a
119+
*/
120+
public function doFoo8(array $a): void
121+
{
122+
foreach ($a as &$v) {
123+
if (rand(0, 1)) {
124+
$v = 1;
125+
}
126+
}
127+
128+
assertType('array<int, 1|string>', $a);
129+
}
130+
131+
}

0 commit comments

Comments
 (0)