Skip to content

Commit 9dfe8a5

Browse files
committed
Fix phpstan/phpstan#13773: for loop index inference should only work for lists
- Added isList() check in inferForLoopExpressions() before assigning $array[$i] type - The inference that $array[$i] exists in `for ($i=0; $i<count($array); $i++)` is only valid when $array is a list (sequential integer keys starting from 0) - New regression test in tests/PHPStan/Rules/Arrays/data/bug-13773.php
1 parent b24bd5e commit 9dfe8a5

3 files changed

Lines changed: 74 additions & 10 deletions

File tree

src/Analyser/NodeScopeResolver.php

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4545,11 +4545,15 @@ private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, Mutatin
45454545
&& $stmt->init[0]->var->name === $lastCondExpr->left->name
45464546
) {
45474547
$arrayArg = $lastCondExpr->right->getArgs()[0]->value;
4548-
$bodyScope = $bodyScope->assignExpression(
4549-
new ArrayDimFetch($lastCondExpr->right->getArgs()[0]->value, $lastCondExpr->left),
4550-
$bodyScope->getType($arrayArg)->getIterableValueType(),
4551-
$bodyScope->getNativeType($arrayArg)->getIterableValueType(),
4552-
);
4548+
$arrayType = $bodyScope->getType($arrayArg);
4549+
$arrayNativeType = $bodyScope->getNativeType($arrayArg);
4550+
if ($arrayType->isList()->yes() || $arrayNativeType->isList()->yes()) {
4551+
$bodyScope = $bodyScope->assignExpression(
4552+
new ArrayDimFetch($lastCondExpr->right->getArgs()[0]->value, $lastCondExpr->left),
4553+
$arrayType->getIterableValueType(),
4554+
$arrayNativeType->getIterableValueType(),
4555+
);
4556+
}
45534557
}
45544558

45554559
// count($items) > $i
@@ -4566,11 +4570,15 @@ private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, Mutatin
45664570
&& $stmt->init[0]->var->name === $lastCondExpr->right->name
45674571
) {
45684572
$arrayArg = $lastCondExpr->left->getArgs()[0]->value;
4569-
$bodyScope = $bodyScope->assignExpression(
4570-
new ArrayDimFetch($lastCondExpr->left->getArgs()[0]->value, $lastCondExpr->right),
4571-
$bodyScope->getType($arrayArg)->getIterableValueType(),
4572-
$bodyScope->getNativeType($arrayArg)->getIterableValueType(),
4573-
);
4573+
$arrayType = $bodyScope->getType($arrayArg);
4574+
$arrayNativeType = $bodyScope->getNativeType($arrayArg);
4575+
if ($arrayType->isList()->yes() || $arrayNativeType->isList()->yes()) {
4576+
$bodyScope = $bodyScope->assignExpression(
4577+
new ArrayDimFetch($lastCondExpr->left->getArgs()[0]->value, $lastCondExpr->right),
4578+
$arrayType->getIterableValueType(),
4579+
$arrayNativeType->getIterableValueType(),
4580+
);
4581+
}
45744582
}
45754583
}
45764584

tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,4 +1240,20 @@ public function testBug14234(): void
12401240
]);
12411241
}
12421242

1243+
public function testBug13773(): void
1244+
{
1245+
$this->reportPossiblyNonexistentGeneralArrayOffset = true;
1246+
1247+
$this->analyse([__DIR__ . '/data/bug-13773.php'], [
1248+
[
1249+
'Offset int<0, max> might not exist on non-empty-array<int, string>.',
1250+
14,
1251+
],
1252+
[
1253+
'Offset int<0, max> might not exist on non-empty-array<int, string>.',
1254+
38,
1255+
],
1256+
]);
1257+
}
1258+
12431259
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13773;
4+
5+
/** @return array<int, string> */
6+
function getArray(): array {
7+
return [100 => "hey"];
8+
}
9+
10+
function testNonListArray(): void
11+
{
12+
$array = getArray();
13+
for ($i = 0; $i < count($array); $i++) {
14+
$a = $array[$i];
15+
}
16+
}
17+
18+
/** @param list<string> $list */
19+
function testList(array $list): void
20+
{
21+
for ($i = 0; $i < count($list); $i++) {
22+
$a = $list[$i];
23+
}
24+
}
25+
26+
/** @param list<string> $list */
27+
function testListReversed(array $list): void
28+
{
29+
for ($i = 0; count($list) > $i; ++$i) {
30+
$a = $list[$i];
31+
}
32+
}
33+
34+
/** @param array<int, string> $array */
35+
function testNonListReversed(array $array): void
36+
{
37+
for ($i = 0; count($array) > $i; ++$i) {
38+
$a = $array[$i];
39+
}
40+
}

0 commit comments

Comments
 (0)