Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,22 @@ public function specifyTypesInCondition(
);
}
}

// infer $list[$index] after $index < count($list)
if (
Copy link
Contributor

Choose a reason for hiding this comment

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

While it seems correct, I feel like it should be in the specifyTypesForCountFuncCall method.

Copy link
Contributor

Choose a reason for hiding this comment

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

specifyTypesForCountFuncCall has some complex expressions already and the if-case here seems independent from what happens in specifyTypesForCountFuncCall.

do you have already something specific in mind? I feel like keeping this if here separate is easier to follow

Copy link
Contributor

Choose a reason for hiding this comment

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

No I had nothing in mind, but was surprised of the

&& !$leftType instanceof ConstantIntegerType

and understood it was maybe handled in specifyTypesForCountFuncCall, so it would have been clearer to group them.

$context->true()
&& !$orEqual
// constant offsets are handled via HasOffsetType/HasOffsetValueType
&& !$leftType instanceof ConstantIntegerType
&& $argType->isList()->yes()
&& IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes()
) {
$arrayArg = $expr->right->getArgs()[0]->value;
$dimFetch = new ArrayDimFetch($arrayArg, $expr->left);
$result = $result->unionWith(
$this->create($dimFetch, $argType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr),
);
}
}

if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1155,4 +1155,32 @@ public function testBug11276(): void
$this->analyse([__DIR__ . '/data/bug-11276.php'], []);
}

public function testBug13770(): void
{
$this->reportPossiblyNonexistentGeneralArrayOffset = true;

$this->analyse([__DIR__ . '/data/bug-13770.php'], [
[
'Offset int<1, max> might not exist on non-empty-list<int>.',
53,
],
[
'Offset int might not exist on list<int>.',
66,
],
[
'Offset int might not exist on list<int>.',
91,
],
[
'Offset int might not exist on array.',
100,
],
[
'Offset -1|3|6|10 might not exist on list<int>.',
126,
],
]);
}

}
131 changes: 131 additions & 0 deletions tests/PHPStan/Rules/Arrays/data/bug-13770.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php declare(strict_types = 1);

namespace Bug13770;

class HelloWorld
{
/**
* @param list<int> $array
* @param positive-int $index
*/
public function positiveIntLessThanCount(array $array, int $index): int
{
if ($index < count($array)) {
return $array[$index]; // should not report
}

return 0;
}

/**
* @param list<int> $array
* @param positive-int $index
*/
public function positiveIntLessThanCountInversed(array $array, int $index): int
{
if (count($array) > $index) {
return $array[$index]; // should not report
}

return 0;
}

/**
* @param list<int> $array
* @param int<0, max> $index
*/
public function nonNegativeIntLessThanCount(array $array, int $index): int
{
if ($index < count($array)) {
return $array[$index]; // should not report
}

return 0;
}

/**
* @param list<int> $array
* @param positive-int $index
*/
public function positiveIntLessThanOrEqualCount(array $array, int $index): int
{
if ($index <= count($array)) {
return $array[$index]; // SHOULD still report - off by one
}

return 0;
}

/**
* @param list<int> $array
* @param int $index
*/
public function anyIntLessThanCount(array $array, int $index): int
{
if ($index < count($array)) {
return $array[$index]; // SHOULD still report - could be negative
}

return 0;
}

/**
* @param list<int> $array
* @param int<0, max> $index
*/
public function positiveIntOnNormalCountMode(array $array, int $index): int
{
if ($index < count($array, COUNT_NORMAL)) {
return $array[$index]; // should not error
}

return 0;
}

/**
* @param list<int> $array
*/
public function anyIntOnUnknownCountMode(array $array, int $index, $countMode): int
{
if ($index < count($array, $countMode)) {
return $array[$index]; // SHOULD still report - could be negative
}

return 0;
}

public function anyIntOnRecursiveCount(array $array, int $index): int
{
if ($index < count($array, COUNT_RECURSIVE)) {
return $array[$index]; // SHOULD still report - could be negative
}

return 0;
}

/**
* @param list<int> $array
* @param 3|6|10 $index
*/
public function constantPositiveIntLessThanCount(array $array, int $index): int
{
if ($index < count($array)) {
return $array[$index]; // should not report
}

return 0;
}

/**
* @param list<int> $array
* @param -1|3|6|10 $index
*/
public function constantMaybeNegativeIntLessThanCount(array $array, int $index): int
{
if ($index < count($array)) {
return $array[$index]; // SHOULD still report - could be negative
}

return 0;
}
}
Loading