Skip to content

Commit 0242d48

Browse files
authored
Fix incorrect analyzing of array_shift with non-empty-list property
1 parent 44deb6b commit 0242d48

18 files changed

+534
-12
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2664,11 +2664,29 @@ static function (): void {
26642664
$arrayArgNativeType = $scope->getNativeType($arrayArg);
26652665

26662666
$isArrayPop = $functionReflection->getName() === 'array_pop';
2667+
$newType = $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray();
26672668
$scope = $scope->invalidateExpression($arrayArg)->assignExpression(
26682669
$arrayArg,
2669-
$isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(),
2670+
$newType,
26702671
$isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(),
26712672
);
2673+
2674+
$scope = $this->processAssignVar(
2675+
$scope,
2676+
$stmt,
2677+
$arrayArg,
2678+
new TypeExpr($newType),
2679+
static function (Node $node, Scope $scope) use ($nodeCallback): void {
2680+
if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
2681+
return;
2682+
}
2683+
2684+
$nodeCallback($node, $scope);
2685+
},
2686+
$context,
2687+
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []),
2688+
true,
2689+
)->getScope();
26722690
}
26732691

26742692
if (
@@ -2681,6 +2699,23 @@ static function (): void {
26812699

26822700
$arrayArg = $expr->getArgs()[0]->value;
26832701
$scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $arrayNativeType);
2702+
2703+
$scope = $this->processAssignVar(
2704+
$scope,
2705+
$stmt,
2706+
$arrayArg,
2707+
new TypeExpr($arrayType),
2708+
static function (Node $node, Scope $scope) use ($nodeCallback): void {
2709+
if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
2710+
return;
2711+
}
2712+
2713+
$nodeCallback($node, $scope);
2714+
},
2715+
$context,
2716+
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []),
2717+
true,
2718+
)->getScope();
26842719
}
26852720

26862721
if (
@@ -2695,11 +2730,29 @@ static function (): void {
26952730
&& $functionReflection->getName() === 'shuffle'
26962731
) {
26972732
$arrayArg = $expr->getArgs()[0]->value;
2733+
$newType = $scope->getType($arrayArg)->shuffleArray();
26982734
$scope = $scope->assignExpression(
26992735
$arrayArg,
2700-
$scope->getType($arrayArg)->shuffleArray(),
2736+
$newType,
27012737
$scope->getNativeType($arrayArg)->shuffleArray(),
27022738
);
2739+
2740+
$scope = $this->processAssignVar(
2741+
$scope,
2742+
$stmt,
2743+
$arrayArg,
2744+
new TypeExpr($newType),
2745+
static function (Node $node, Scope $scope) use ($nodeCallback): void {
2746+
if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
2747+
return;
2748+
}
2749+
2750+
$nodeCallback($node, $scope);
2751+
},
2752+
$context,
2753+
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []),
2754+
true,
2755+
)->getScope();
27032756
}
27042757

27052758
if (
@@ -2715,11 +2768,29 @@ static function (): void {
27152768
$lengthType = isset($expr->getArgs()[2]) ? $scope->getType($expr->getArgs()[2]->value) : new NullType();
27162769
$replacementType = isset($expr->getArgs()[3]) ? $scope->getType($expr->getArgs()[3]->value) : new ConstantArrayType([], []);
27172770

2771+
$newType = $arrayArgType->spliceArray($offsetType, $lengthType, $replacementType);
27182772
$scope = $scope->invalidateExpression($arrayArg)->assignExpression(
27192773
$arrayArg,
2720-
$arrayArgType->spliceArray($offsetType, $lengthType, $replacementType),
2774+
$newType,
27212775
$arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType),
27222776
);
2777+
2778+
$scope = $this->processAssignVar(
2779+
$scope,
2780+
$stmt,
2781+
$arrayArg,
2782+
new TypeExpr($newType),
2783+
static function (Node $node, Scope $scope) use ($nodeCallback): void {
2784+
if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
2785+
return;
2786+
}
2787+
2788+
$nodeCallback($node, $scope);
2789+
},
2790+
$context,
2791+
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []),
2792+
true,
2793+
)->getScope();
27232794
}
27242795

27252796
if (
@@ -2728,11 +2799,29 @@ static function (): void {
27282799
&& count($expr->getArgs()) >= 1
27292800
) {
27302801
$arrayArg = $expr->getArgs()[0]->value;
2802+
$newType = $this->getArraySortPreserveListFunctionType($scope->getType($arrayArg));
27312803
$scope = $scope->assignExpression(
27322804
$arrayArg,
2733-
$this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)),
2805+
$newType,
27342806
$this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg)),
27352807
);
2808+
2809+
$scope = $this->processAssignVar(
2810+
$scope,
2811+
$stmt,
2812+
$arrayArg,
2813+
new TypeExpr($newType),
2814+
static function (Node $node, Scope $scope) use ($nodeCallback): void {
2815+
if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
2816+
return;
2817+
}
2818+
2819+
$nodeCallback($node, $scope);
2820+
},
2821+
$context,
2822+
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []),
2823+
true,
2824+
)->getScope();
27362825
}
27372826

27382827
if (
@@ -2741,11 +2830,29 @@ static function (): void {
27412830
&& count($expr->getArgs()) >= 1
27422831
) {
27432832
$arrayArg = $expr->getArgs()[0]->value;
2833+
$newType = $this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg));
27442834
$scope = $scope->assignExpression(
27452835
$arrayArg,
2746-
$this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)),
2836+
$newType,
27472837
$this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg)),
27482838
);
2839+
2840+
$scope = $this->processAssignVar(
2841+
$scope,
2842+
$stmt,
2843+
$arrayArg,
2844+
new TypeExpr($newType),
2845+
static function (Node $node, Scope $scope) use ($nodeCallback): void {
2846+
if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
2847+
return;
2848+
}
2849+
2850+
$nodeCallback($node, $scope);
2851+
},
2852+
$context,
2853+
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []),
2854+
true,
2855+
)->getScope();
27492856
}
27502857

27512858
if (
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Bug11846;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function demo(): void
8+
{
9+
$outerList = [];
10+
$idList = [1, 2];
11+
12+
foreach ($idList as $id) {
13+
$outerList[$id] = [];
14+
array_push($outerList[$id], []);
15+
}
16+
assertType('non-empty-array<1|2, array{}|array{array{}}>', $outerList);
17+
18+
foreach ($outerList as $key => $outerElement) {
19+
$result = false;
20+
21+
assertType('array{}|array{array{}}', $outerElement);
22+
foreach ($outerElement as $innerElement) {
23+
$result = true;
24+
}
25+
assertType('bool', $result); // could be 'true'
26+
27+
}
28+
}

tests/PHPStan/Analyser/nsrt/shuffle.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public function normalArrays1(array $arr): void
1313
/** @var mixed[] $arr */
1414
shuffle($arr);
1515
assertType('list<mixed>', $arr);
16-
assertNativeType('list', $arr);
16+
assertNativeType('list<mixed>', $arr);
1717
assertType('list<int<0, max>>', array_keys($arr));
1818
assertType('list<mixed>', array_values($arr));
1919
}
@@ -23,7 +23,7 @@ public function normalArrays2(array $arr): void
2323
/** @var non-empty-array<string, int> $arr */
2424
shuffle($arr);
2525
assertType('non-empty-list<int>', $arr);
26-
assertNativeType('list', $arr);
26+
assertNativeType('non-empty-list<int>', $arr);
2727
assertType('non-empty-list<int<0, max>>', array_keys($arr));
2828
assertType('non-empty-list<int>', array_values($arr));
2929
}
@@ -67,7 +67,7 @@ public function constantArrays2(array $arr): void
6767
/** @var array{0?: 1, 1?: 2, 2?: 3} $arr */
6868
shuffle($arr);
6969
assertType('list<1|2|3>', $arr);
70-
assertNativeType('list', $arr);
70+
assertNativeType('list<1|2|3>', $arr);
7171
assertType('list<0|1|2>', array_keys($arr));
7272
assertType('list<1|2|3>', array_values($arr));
7373
}
@@ -107,7 +107,7 @@ public function constantArrays6(array $arr): void
107107
/** @var array{foo?: 1, bar: 2, }|array{baz: 3, foobar?: 4} $arr */
108108
shuffle($arr);
109109
assertType('non-empty-list<1|2|3|4>', $arr);
110-
assertNativeType('list', $arr);
110+
assertNativeType('non-empty-list<1|2|3|4>', $arr);
111111
assertType('non-empty-list<0|1>', array_keys($arr));
112112
assertType('non-empty-list<1|2|3|4>', array_values($arr));
113113
}

tests/PHPStan/Analyser/nsrt/sort.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,17 @@ public function normalArray(array $arr): void
9191
$arr1 = $arr;
9292
sort($arr1);
9393
assertType('list<string>', $arr1);
94-
assertNativeType('list', $arr1);
94+
assertNativeType('list<string>', $arr1);
9595

9696
$arr2 = $arr;
9797
rsort($arr2);
9898
assertType('list<string>', $arr2);
99-
assertNativeType('list', $arr2);
99+
assertNativeType('list<string>', $arr2);
100100

101101
$arr3 = $arr;
102102
usort($arr3, fn(int $a, int $b) => $a <=> $b);
103103
assertType('list<string>', $arr3);
104-
assertNativeType('list', $arr3);
104+
assertNativeType('list<string>', $arr3);
105105
}
106106

107107
public function mixed($arr): void

tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,14 @@ public function testBug13248(): void
4545
$this->analyse([__DIR__ . '/data/bug-13248.php'], []);
4646
}
4747

48+
public function testBug2560(): void
49+
{
50+
$this->analyse([__DIR__ . '/data/bug-2560.php'], []);
51+
}
52+
53+
public function testBug2457(): void
54+
{
55+
$this->analyse([__DIR__ . '/data/bug-2457.php'], []);
56+
}
57+
4858
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Bug2457;
4+
5+
class HelloWorld
6+
{
7+
public function sayHello(array $x, array $y): void
8+
{
9+
$a = [];
10+
$o = [];
11+
12+
foreach ($x as $t) {
13+
if (!isset($a[$t])) {
14+
$a[$t] = [];
15+
}
16+
$o[] = 'x';
17+
array_unshift($a[$t], count($o) - 1);
18+
}
19+
20+
foreach ($y as $t) {
21+
if (!isset($a[$t])) {
22+
continue;
23+
}
24+
foreach ($a[$t] as $b) {
25+
// this will be reached
26+
}
27+
}
28+
}
29+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Bug2560;
4+
5+
class HelloWorld
6+
{
7+
public function test(): void
8+
{
9+
$arr = [];
10+
foreach ([0,1] as $i) {
11+
$arr[$i] = [];
12+
array_push($arr[$i], "foo");
13+
}
14+
foreach (array_values($arr) as $vec) {
15+
foreach ($vec as $value) {
16+
print_r("$value");
17+
}
18+
}
19+
}
20+
}

tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,4 +257,10 @@ public function testBug12716(): void
257257
$this->analyse([__DIR__ . '/data/bug-12716.php'], []);
258258
}
259259

260+
public function testBug3387(): void
261+
{
262+
$this->treatPhpDocTypesAsCertain = true;
263+
$this->analyse([__DIR__ . '/data/bug-3387.php'], []);
264+
}
265+
260266
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Bug3387;
4+
5+
function (array $items, string $key) {
6+
$array = [$key => []];
7+
foreach ($items as $item) {
8+
$array[$key][] = $item;
9+
if (count($array[$key]) > 1) {
10+
throw new RuntimeException();
11+
}
12+
}
13+
};
14+
15+
function (array $items, string $key) {
16+
$array = [$key => []];
17+
foreach ($items as $item) {
18+
array_unshift($array[$key], $item);
19+
if (count($array[$key]) > 1) {
20+
throw new RuntimeException();
21+
}
22+
}
23+
};
24+
25+
function (array $items, string $key) {
26+
$array = [$key => []];
27+
foreach ($items as $item) {
28+
array_push($array[$key], $item);
29+
if (count($array[$key]) > 1) {
30+
throw new RuntimeException();
31+
}
32+
}
33+
};

0 commit comments

Comments
 (0)