Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 63 additions & 16 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\ConstantTypeHelper;
use PHPStan\Type\ErrorType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StaticTypeFactory;
Expand Down Expand Up @@ -987,25 +988,11 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar
continue;
}

if (!$arrayDimFetch->dim instanceof Expr\BinaryOp\Plus) {
if (!$this->shouldKeepList($arrayDimFetch, $scope, $offsetValueType)) {
continue;
}

if ( // keep list for $list[$index + 1] assignments
$arrayDimFetch->dim->right instanceof Variable
&& $arrayDimFetch->dim->left instanceof Node\Scalar\Int_
&& $arrayDimFetch->dim->left->value === 1
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->right))->yes()
) {
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType());
} elseif ( // keep list for $list[1 + $index] assignments
$arrayDimFetch->dim->left instanceof Variable
&& $arrayDimFetch->dim->right instanceof Node\Scalar\Int_
&& $arrayDimFetch->dim->right->value === 1
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes()
) {
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType());
}
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType());
}

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

private function shouldKeepList(ArrayDimFetch $arrayDimFetch, Scope $scope, Type $offsetValueType): bool
{
if ($arrayDimFetch->dim instanceof Expr\BinaryOp\Plus) {
if ( // keep list for $list[$index + 1] assignments
$arrayDimFetch->dim->right instanceof Variable
&& $arrayDimFetch->dim->left instanceof Node\Scalar\Int_
&& $arrayDimFetch->dim->left->value === 1
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->right))->yes()
) {
return true;
} elseif ( // keep list for $list[1 + $index] assignments
$arrayDimFetch->dim->left instanceof Variable
&& $arrayDimFetch->dim->right instanceof Node\Scalar\Int_
&& $arrayDimFetch->dim->right->value === 1
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes()
) {
return true;
}
} elseif ( // keep list for $list[count($list) - n] assignments
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cant we risk having n too big and set the offset -1 ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

since the logic here will only add non-empty-list to a pre-existing maybe already degraded because too huge array, I don't see how a too big "n" value can be a problem here.

added a test with a huge array and a big offset

$arrayDimFetch->dim instanceof Expr\BinaryOp\Minus
&& $arrayDimFetch->dim->right instanceof Node\Scalar\Int_
&& $arrayDimFetch->dim->left instanceof Expr\FuncCall
&& $arrayDimFetch->dim->left->name instanceof Name
&& in_array($arrayDimFetch->dim->left->name->toLowerString(), ['count', 'sizeof'], true)
&& count($arrayDimFetch->dim->left->getArgs()) === 1 // could support COUNT_RECURSIVE, COUNT_NORMAL
&& $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->left->getArgs()[0]->value)
&& IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($arrayDimFetch->dim))->yes()
&& $offsetValueType->isIterableAtLeastOnce()->yes()
) {
return true;
} elseif ( // keep list for $list[array_key_last($list)] and $list[array_key_first($list)] assignments
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Array_search works too

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

added 👍

$arrayDimFetch->dim instanceof Expr\FuncCall
&& $arrayDimFetch->dim->name instanceof Name
&& in_array($arrayDimFetch->dim->name->toLowerString(), ['array_key_last', 'array_key_first'], true)
&& count($arrayDimFetch->dim->getArgs()) >= 1
&& $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->getArgs()[0]->value)
) {
return true;
} elseif ( // keep list for $list[array_search($needle, $list)] assignments
$arrayDimFetch->dim instanceof Expr\FuncCall
&& $arrayDimFetch->dim->name instanceof Name
&& $arrayDimFetch->dim->name->toLowerString() === 'array_search'
&& count($arrayDimFetch->dim->getArgs()) >= 1
&& $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->getArgs()[1]->value)
) {
return true;
}

return false;
}

private function isSameVariable(Expr $a, Expr $b): bool
{
if ($a instanceof Variable && $b instanceof Variable && is_string($a->name) && is_string($b->name)) {
return $a->name === $b->name;
}

return false;
}

}
137 changes: 137 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14245.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php declare(strict_types = 1);

namespace Bug14245;

use function array_key_first;
use function array_key_last;
use function PHPStan\Testing\assertType;

/**
* @return list<int>
*/
function foo(): array {
return [];
}

function doFoo(): void {
$list = foo();
$count = count($list);
assertType('list<int>', $list);
if ($count > 0) {
assertType('non-empty-list<int>', $list);
$list[count($list) - 1] = 37;
assertType('non-empty-list<int>', $list);
}

assertType('list<int>', $list);
}

function doFoo2(): void {
$list = foo();
$count = count($list);
assertType('list<int>', $list);
if ($count > 0) {
assertType('non-empty-list<int>', $list);
// we don't know the $list length,
// therefore count() - N might be before the first element -> degrade to array
$list[count($list) - 5] = 37;
assertType('non-empty-array<int<-4, max>, int>', $list);
}

assertType('array<int<-4, max>, int>', $list);
}

function listKnownSize(): void {
$list = foo();
assertType('list<int>', $list);
if (count($list) === 5) {
assertType('array{int, int, int, int, int}', $list);
$list[count($list) - 3] = 37;
assertType('array{int, int, 37, int, int}', $list);
}

assertType('list<int>', $list);
}

function listKnownHugeSize(): void {
$list = foo();
assertType('list<int>', $list);
if (count($list) === 50000) {
assertType('non-empty-list<int>', $list);
$list[count($list) - 3000] = 37;
assertType('non-empty-array<int<-2999, max>, int>', $list);
}

assertType('array<int<-2999, max>, int>', $list);
}

function overwriteKeyLast(): void {
$list = foo();
$count = count($list);
assertType('list<int>', $list);
if ($count > 0) {
assertType('non-empty-list<int>', $list);
$list[array_key_last($list)] = 37;
assertType('non-empty-list<int>', $list);
}

assertType('list<int>', $list);
}

function overwriteKeyFirst(): void {
$list = foo();
$count = count($list);
assertType('list<int>', $list);
if ($count > 0) {
assertType('non-empty-list<int>', $list);
$list[array_key_first($list)] = 37;
assertType('non-empty-list<int>', $list);
}

assertType('list<int>', $list);
}

function overwriteKeyFirstMaybeEmptyArray(): void {
$list = foo();
assertType('list<int>', $list);
// empty list might return NULL for array_key_first()
$list[array_key_first($list)] = 37;
assertType('non-empty-list<int>', $list);
}

function keyDifferentArray(array $arr): void {
$list = foo();
assertType('list<int>', $list);
$list[array_key_first($arr)] = 37;
assertType('non-empty-array<int|string, int>', $list);
}

function overwriteArraySearch($needle): void {
$list = foo();

assertType('list<int>', $list);
// search in empty-array, or with a non-existent key will return false,
// which gets auto-casted to 0, so we still have a list
// https://3v4l.org/RZbOK
$list[array_search($needle, $list)] = 37;
assertType('non-empty-list<int>', $list);
}

function overwriteArraySearchStrict($needle): void {
$list = foo();

assertType('list<int>', $list);
// search in empty-array, or with a non-existent key will return false,
// which gets auto-casted to 0, so we still have a list
// https://3v4l.org/RZbOK
$list[array_search($needle, $list, true)] = 37;
assertType('non-empty-list<int>', $list);
}

function ArraySearchWithDifferentArray($array2, $needle): void {
$list = foo();

assertType('list<int>', $list);
$list[array_search($needle, $array2, true)] = 37;
assertType('non-empty-array<int|string, int>', $list);
}
Loading