Skip to content
117 changes: 112 additions & 5 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2664,11 +2664,29 @@ static function (): void {
$arrayArgNativeType = $scope->getNativeType($arrayArg);

$isArrayPop = $functionReflection->getName() === 'array_pop';
$newType = $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray();
$scope = $scope->invalidateExpression($arrayArg)->assignExpression(
$arrayArg,
$isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(),
$newType,
$isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(),
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have also tried removing aboves $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, ...) but this leads to different native vs. phpdoc type results

--- a/src/Analyser/NodeScopeResolver.php
+++ b/src/Analyser/NodeScopeResolver.php
@@ -2659,22 +2659,14 @@ final class NodeScopeResolver
                                && count($expr->getArgs()) >= 1
                        ) {
                                $arrayArg = $expr->getArgs()[0]->value;
-
                                $arrayArgType = $scope->getType($arrayArg);
-                               $arrayArgNativeType = $scope->getNativeType($arrayArg);
-
                                $isArrayPop = $functionReflection->getName() === 'array_pop';
-                               $scope = $scope->invalidateExpression($arrayArg)->assignExpression(
-                                       $arrayArg,
-                                       $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(),
-                                       $isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(),
-                               );
 
                                $scope = $this->processAssignVar(
                                        $scope,
                                        $stmt,
                                        $arrayArg,
-                                       $arrayArg,
+                                       new TypeExpr($isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray()),
                                        static function (Node $node, Scope $scope) use ($nodeCallback): void {
                                                if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with that as a temporary step.

Maybe we can experiment with a new TypeExpr-like class in another PR that would carry $type and $nativeType - two constructor arguments. A bunch of places that currently do new TypeExpr might get better behaviour from that.

Copy link
Contributor Author

@staabm staabm Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after doing #4294 (comment) we get the same type changes, as I saw with the above suggestion.

so I went ahead and did this simplification in a53b7a9

if you are not fine with it, I can revert the changes (but it does not affect results of tests)

Maybe we can experiment with a new TypeExpr-like class in another PR that would carry $type and $nativeType - two constructor arguments

agree 👍

Copy link
Member

@ondrejmirtes ondrejmirtes Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want this simplification. We'd have to put the code back anyway after we do the "TypeExpr with native type" again. Please revert it.

$scope = $this->processAssignVar(
$scope,
$stmt,
$arrayArg,
new TypeExpr($newType),
static function (Node $node, Scope $scope) use ($nodeCallback): void {
if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
return;
}

$nodeCallback($node, $scope);
},
$context,
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []),
true,
)->getScope();
}

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

$arrayArg = $expr->getArgs()[0]->value;
$scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $arrayNativeType);

$scope = $this->processAssignVar(
$scope,
$stmt,
$arrayArg,
new TypeExpr($arrayType),
static function (Node $node, Scope $scope) use ($nodeCallback): void {
if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
return;
}

$nodeCallback($node, $scope);
},
$context,
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []),
true,
)->getScope();
}

if (
Expand All @@ -2695,11 +2730,29 @@ static function (): void {
&& $functionReflection->getName() === 'shuffle'
) {
$arrayArg = $expr->getArgs()[0]->value;
$newType = $scope->getType($arrayArg)->shuffleArray();
$scope = $scope->assignExpression(
$arrayArg,
$scope->getType($arrayArg)->shuffleArray(),
$newType,
$scope->getNativeType($arrayArg)->shuffleArray(),
);

$scope = $this->processAssignVar(
$scope,
$stmt,
$arrayArg,
new TypeExpr($newType),
static function (Node $node, Scope $scope) use ($nodeCallback): void {
if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
return;
}

$nodeCallback($node, $scope);
},
$context,
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []),
true,
)->getScope();
}

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

$newType = $arrayArgType->spliceArray($offsetType, $lengthType, $replacementType);
$scope = $scope->invalidateExpression($arrayArg)->assignExpression(
$arrayArg,
$arrayArgType->spliceArray($offsetType, $lengthType, $replacementType),
$newType,
$arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType),
);

$scope = $this->processAssignVar(
$scope,
$stmt,
$arrayArg,
new TypeExpr($newType),
static function (Node $node, Scope $scope) use ($nodeCallback): void {
if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
return;
}

$nodeCallback($node, $scope);
},
$context,
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []),
true,
)->getScope();
}

if (
Expand All @@ -2728,11 +2799,29 @@ static function (): void {
&& count($expr->getArgs()) >= 1
) {
$arrayArg = $expr->getArgs()[0]->value;
$newType = $this->getArraySortPreserveListFunctionType($scope->getType($arrayArg));
$scope = $scope->assignExpression(
$arrayArg,
$this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)),
$newType,
$this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg)),
);

$scope = $this->processAssignVar(
$scope,
$stmt,
$arrayArg,
new TypeExpr($newType),
static function (Node $node, Scope $scope) use ($nodeCallback): void {
if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
return;
}

$nodeCallback($node, $scope);
},
$context,
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []),
true,
)->getScope();
}

if (
Expand All @@ -2741,11 +2830,29 @@ static function (): void {
&& count($expr->getArgs()) >= 1
) {
$arrayArg = $expr->getArgs()[0]->value;
$newType = $this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg));
$scope = $scope->assignExpression(
$arrayArg,
$this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)),
$newType,
$this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg)),
);

$scope = $this->processAssignVar(
$scope,
$stmt,
$arrayArg,
new TypeExpr($newType),
static function (Node $node, Scope $scope) use ($nodeCallback): void {
if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
return;
}

$nodeCallback($node, $scope);
},
$context,
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []),
true,
)->getScope();
}

if (
Expand Down
28 changes: 28 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11846.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Bug11846;

use function PHPStan\Testing\assertType;

function demo(): void
{
$outerList = [];
$idList = [1, 2];

foreach ($idList as $id) {
$outerList[$id] = [];
array_push($outerList[$id], []);
}
assertType('non-empty-array<1|2, array{}|array{array{}}>', $outerList);

foreach ($outerList as $key => $outerElement) {
$result = false;

assertType('array{}|array{array{}}', $outerElement);
foreach ($outerElement as $innerElement) {
$result = true;
}
assertType('bool', $result); // could be 'true'

}
}
8 changes: 4 additions & 4 deletions tests/PHPStan/Analyser/nsrt/shuffle.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public function normalArrays1(array $arr): void
/** @var mixed[] $arr */
shuffle($arr);
assertType('list<mixed>', $arr);
assertNativeType('list', $arr);
assertNativeType('list<mixed>', $arr);
assertType('list<int<0, max>>', array_keys($arr));
assertType('list<mixed>', array_values($arr));
}
Expand All @@ -23,7 +23,7 @@ public function normalArrays2(array $arr): void
/** @var non-empty-array<string, int> $arr */
shuffle($arr);
assertType('non-empty-list<int>', $arr);
assertNativeType('list', $arr);
assertNativeType('non-empty-list<int>', $arr);
assertType('non-empty-list<int<0, max>>', array_keys($arr));
assertType('non-empty-list<int>', array_values($arr));
}
Expand Down Expand Up @@ -67,7 +67,7 @@ public function constantArrays2(array $arr): void
/** @var array{0?: 1, 1?: 2, 2?: 3} $arr */
shuffle($arr);
assertType('list<1|2|3>', $arr);
assertNativeType('list', $arr);
assertNativeType('list<1|2|3>', $arr);
assertType('list<0|1|2>', array_keys($arr));
assertType('list<1|2|3>', array_values($arr));
}
Expand Down Expand Up @@ -107,7 +107,7 @@ public function constantArrays6(array $arr): void
/** @var array{foo?: 1, bar: 2, }|array{baz: 3, foobar?: 4} $arr */
shuffle($arr);
assertType('non-empty-list<1|2|3|4>', $arr);
assertNativeType('list', $arr);
assertNativeType('non-empty-list<1|2|3|4>', $arr);
assertType('non-empty-list<0|1>', array_keys($arr));
assertType('non-empty-list<1|2|3|4>', array_values($arr));
}
Expand Down
6 changes: 3 additions & 3 deletions tests/PHPStan/Analyser/nsrt/sort.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,17 @@ public function normalArray(array $arr): void
$arr1 = $arr;
sort($arr1);
assertType('list<string>', $arr1);
assertNativeType('list', $arr1);
assertNativeType('list<string>', $arr1);

$arr2 = $arr;
rsort($arr2);
assertType('list<string>', $arr2);
assertNativeType('list', $arr2);
assertNativeType('list<string>', $arr2);

$arr3 = $arr;
usort($arr3, fn(int $a, int $b) => $a <=> $b);
assertType('list<string>', $arr3);
assertNativeType('list', $arr3);
assertNativeType('list<string>', $arr3);
}

public function mixed($arr): void
Expand Down
10 changes: 10 additions & 0 deletions tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,14 @@ public function testBug13248(): void
$this->analyse([__DIR__ . '/data/bug-13248.php'], []);
}

public function testBug2560(): void
{
$this->analyse([__DIR__ . '/data/bug-2560.php'], []);
}

public function testBug2457(): void
{
$this->analyse([__DIR__ . '/data/bug-2457.php'], []);
}

}
29 changes: 29 additions & 0 deletions tests/PHPStan/Rules/Arrays/data/bug-2457.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Bug2457;

class HelloWorld
{
public function sayHello(array $x, array $y): void
{
$a = [];
$o = [];

foreach ($x as $t) {
if (!isset($a[$t])) {
$a[$t] = [];
}
$o[] = 'x';
array_unshift($a[$t], count($o) - 1);
}

foreach ($y as $t) {
if (!isset($a[$t])) {
continue;
}
foreach ($a[$t] as $b) {
// this will be reached
}
}
}
}
20 changes: 20 additions & 0 deletions tests/PHPStan/Rules/Arrays/data/bug-2560.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Bug2560;

class HelloWorld
{
public function test(): void
{
$arr = [];
foreach ([0,1] as $i) {
$arr[$i] = [];
array_push($arr[$i], "foo");
}
foreach (array_values($arr) as $vec) {
foreach ($vec as $value) {
print_r("$value");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,10 @@ public function testBug12716(): void
$this->analyse([__DIR__ . '/data/bug-12716.php'], []);
}

public function testBug3387(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-3387.php'], []);
}

}
33 changes: 33 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-3387.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Bug3387;

function (array $items, string $key) {
$array = [$key => []];
foreach ($items as $item) {
$array[$key][] = $item;
if (count($array[$key]) > 1) {
throw new RuntimeException();
}
}
};

function (array $items, string $key) {
$array = [$key => []];
foreach ($items as $item) {
array_unshift($array[$key], $item);
if (count($array[$key]) > 1) {
throw new RuntimeException();
}
}
};

function (array $items, string $key) {
$array = [$key => []];
foreach ($items as $item) {
array_push($array[$key], $item);
if (count($array[$key]) > 1) {
throw new RuntimeException();
}
}
};
Loading
Loading