Skip to content

Commit 050e6bf

Browse files
phpstan-botstaabm
andauthored
Fix phpstan/phpstan#14245: list lost after overwriting last element (#5169)
Co-authored-by: Markus Staab <markus.staab@redaxo.de> Co-authored-by: Markus Staab <maggus.staab@googlemail.com>
1 parent 54f3522 commit 050e6bf

File tree

2 files changed

+211
-16
lines changed

2 files changed

+211
-16
lines changed

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
use PHPStan\Type\Constant\ConstantStringType;
5454
use PHPStan\Type\ConstantTypeHelper;
5555
use PHPStan\Type\ErrorType;
56+
use PHPStan\Type\IntegerRangeType;
5657
use PHPStan\Type\MixedType;
5758
use PHPStan\Type\ObjectType;
5859
use PHPStan\Type\StaticTypeFactory;
@@ -987,25 +988,11 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar
987988
continue;
988989
}
989990

990-
if (!$arrayDimFetch->dim instanceof Expr\BinaryOp\Plus) {
991+
if (!$this->shouldKeepList($arrayDimFetch, $scope, $offsetValueType)) {
991992
continue;
992993
}
993994

994-
if ( // keep list for $list[$index + 1] assignments
995-
$arrayDimFetch->dim->right instanceof Variable
996-
&& $arrayDimFetch->dim->left instanceof Node\Scalar\Int_
997-
&& $arrayDimFetch->dim->left->value === 1
998-
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->right))->yes()
999-
) {
1000-
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType());
1001-
} elseif ( // keep list for $list[1 + $index] assignments
1002-
$arrayDimFetch->dim->left instanceof Variable
1003-
&& $arrayDimFetch->dim->right instanceof Node\Scalar\Int_
1004-
&& $arrayDimFetch->dim->right->value === 1
1005-
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes()
1006-
) {
1007-
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType());
1008-
}
995+
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType());
1009996
}
1010997

1011998
$additionalExpressions = [];
@@ -1029,4 +1016,64 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar
10291016
return [$valueToWrite, $additionalExpressions];
10301017
}
10311018

1019+
private function shouldKeepList(ArrayDimFetch $arrayDimFetch, Scope $scope, Type $offsetValueType): bool
1020+
{
1021+
if ($arrayDimFetch->dim instanceof Expr\BinaryOp\Plus) {
1022+
if ( // keep list for $list[$index + 1] assignments
1023+
$arrayDimFetch->dim->right instanceof Variable
1024+
&& $arrayDimFetch->dim->left instanceof Node\Scalar\Int_
1025+
&& $arrayDimFetch->dim->left->value === 1
1026+
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->right))->yes()
1027+
) {
1028+
return true;
1029+
} elseif ( // keep list for $list[1 + $index] assignments
1030+
$arrayDimFetch->dim->left instanceof Variable
1031+
&& $arrayDimFetch->dim->right instanceof Node\Scalar\Int_
1032+
&& $arrayDimFetch->dim->right->value === 1
1033+
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes()
1034+
) {
1035+
return true;
1036+
}
1037+
} elseif ( // keep list for $list[count($list) - n] assignments
1038+
$arrayDimFetch->dim instanceof Expr\BinaryOp\Minus
1039+
&& $arrayDimFetch->dim->right instanceof Node\Scalar\Int_
1040+
&& $arrayDimFetch->dim->left instanceof Expr\FuncCall
1041+
&& $arrayDimFetch->dim->left->name instanceof Name
1042+
&& in_array($arrayDimFetch->dim->left->name->toLowerString(), ['count', 'sizeof'], true)
1043+
&& count($arrayDimFetch->dim->left->getArgs()) === 1 // could support COUNT_RECURSIVE, COUNT_NORMAL
1044+
&& $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->left->getArgs()[0]->value)
1045+
&& IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($arrayDimFetch->dim))->yes()
1046+
&& $offsetValueType->isIterableAtLeastOnce()->yes()
1047+
) {
1048+
return true;
1049+
} elseif ( // keep list for $list[array_key_last($list)] and $list[array_key_first($list)] assignments
1050+
$arrayDimFetch->dim instanceof Expr\FuncCall
1051+
&& $arrayDimFetch->dim->name instanceof Name
1052+
&& in_array($arrayDimFetch->dim->name->toLowerString(), ['array_key_last', 'array_key_first'], true)
1053+
&& count($arrayDimFetch->dim->getArgs()) >= 1
1054+
&& $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->getArgs()[0]->value)
1055+
) {
1056+
return true;
1057+
} elseif ( // keep list for $list[array_search($needle, $list)] assignments
1058+
$arrayDimFetch->dim instanceof Expr\FuncCall
1059+
&& $arrayDimFetch->dim->name instanceof Name
1060+
&& $arrayDimFetch->dim->name->toLowerString() === 'array_search'
1061+
&& count($arrayDimFetch->dim->getArgs()) >= 1
1062+
&& $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->getArgs()[1]->value)
1063+
) {
1064+
return true;
1065+
}
1066+
1067+
return false;
1068+
}
1069+
1070+
private function isSameVariable(Expr $a, Expr $b): bool
1071+
{
1072+
if ($a instanceof Variable && $b instanceof Variable && is_string($a->name) && is_string($b->name)) {
1073+
return $a->name === $b->name;
1074+
}
1075+
1076+
return false;
1077+
}
1078+
10321079
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14245;
4+
5+
use function array_key_exists;
6+
use function array_key_first;
7+
use function array_key_last;
8+
use function PHPStan\Testing\assertType;
9+
10+
/**
11+
* @return list<int>
12+
*/
13+
function foo(): array {
14+
return [];
15+
}
16+
17+
function doFoo(): void {
18+
$list = foo();
19+
$count = count($list);
20+
assertType('list<int>', $list);
21+
if ($count > 0) {
22+
assertType('non-empty-list<int>', $list);
23+
$list[count($list) - 1] = 37;
24+
assertType('non-empty-list<int>', $list);
25+
}
26+
27+
assertType('list<int>', $list);
28+
}
29+
30+
function doFoo2(): void {
31+
$list = foo();
32+
$count = count($list);
33+
assertType('list<int>', $list);
34+
if ($count > 0) {
35+
assertType('non-empty-list<int>', $list);
36+
// we don't know the $list length,
37+
// therefore count() - N might be before the first element -> degrade to array
38+
$list[count($list) - 5] = 37;
39+
assertType('non-empty-array<int<-4, max>, int>', $list);
40+
}
41+
42+
assertType('array<int<-4, max>, int>', $list);
43+
}
44+
45+
function listKnownSize(): void {
46+
$list = foo();
47+
assertType('list<int>', $list);
48+
if (count($list) === 5) {
49+
assertType('array{int, int, int, int, int}', $list);
50+
$list[count($list) - 3] = 37;
51+
assertType('array{int, int, 37, int, int}', $list);
52+
}
53+
54+
assertType('list<int>', $list);
55+
}
56+
57+
function listKnownHugeSize(): void {
58+
$list = foo();
59+
assertType('list<int>', $list);
60+
if (count($list) === 50000) {
61+
assertType('non-empty-list<int>', $list);
62+
$list[count($list) - 3000] = 37;
63+
assertType('non-empty-array<int<-2999, max>, int>', $list);
64+
}
65+
66+
assertType('array<int<-2999, max>, int>', $list);
67+
}
68+
69+
function overwriteKeyLast(): void {
70+
$list = foo();
71+
$count = count($list);
72+
assertType('list<int>', $list);
73+
if ($count > 0) {
74+
assertType('non-empty-list<int>', $list);
75+
$list[array_key_last($list)] = 37;
76+
assertType('non-empty-list<int>', $list);
77+
}
78+
79+
assertType('list<int>', $list);
80+
}
81+
82+
function overwriteKeyFirst(): void {
83+
$list = foo();
84+
$count = count($list);
85+
assertType('list<int>', $list);
86+
if ($count > 0) {
87+
assertType('non-empty-list<int>', $list);
88+
$list[array_key_first($list)] = 37;
89+
assertType('non-empty-list<int>', $list);
90+
}
91+
92+
assertType('list<int>', $list);
93+
}
94+
95+
function overwriteKeyFirstMaybeEmptyArray(): void {
96+
$list = foo();
97+
assertType('list<int>', $list);
98+
// empty list might return NULL for array_key_first()
99+
$list[array_key_first($list)] = 37;
100+
assertType('non-empty-list<int>', $list);
101+
}
102+
103+
function keyDifferentArray(array $arr): void {
104+
$list = foo();
105+
assertType('list<int>', $list);
106+
$list[array_key_first($arr)] = 37;
107+
assertType('non-empty-array<int|string, int>', $list);
108+
}
109+
110+
function overwriteArraySearch($needle): void {
111+
$list = foo();
112+
113+
assertType('list<int>', $list);
114+
// search in empty-array, or with a non-existent key will return false,
115+
// which gets auto-casted to 0, so we still have a list
116+
// https://3v4l.org/RZbOK
117+
$list[array_search($needle, $list)] = 37;
118+
assertType('non-empty-list<int>', $list);
119+
}
120+
121+
function overwriteArraySearchStrict($needle): void {
122+
$list = foo();
123+
124+
assertType('list<int>', $list);
125+
// search in empty-array, or with a non-existent key will return false,
126+
// which gets auto-casted to 0, so we still have a list
127+
// https://3v4l.org/RZbOK
128+
$list[array_search($needle, $list, true)] = 37;
129+
assertType('non-empty-list<int>', $list);
130+
}
131+
132+
function ArraySearchWithDifferentArray($array2, $needle): void {
133+
$list = foo();
134+
135+
assertType('list<int>', $list);
136+
$list[array_search($needle, $array2, true)] = 37;
137+
assertType('non-empty-array<int|string, int>', $list);
138+
}
139+
140+
function ArrayKeyExistsKeepsList($needle): void {
141+
$list = foo();
142+
143+
assertType('list<int>', $list);
144+
if (array_key_exists($needle, $list)) {
145+
$list[$needle] = 37;
146+
}
147+
assertType('list<int>', $list);
148+
}

0 commit comments

Comments
 (0)