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
48 changes: 34 additions & 14 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,36 +266,56 @@ public function specifyTypesInCondition(
$leftType = $scope->getType($expr->left);
$result = (new SpecifiedTypes([], []))->setRootExpr($expr);

$countFuncCall = null;
$subtraction = 0;

if (
!$context->null()
&& $expr->right instanceof FuncCall
$expr->right instanceof FuncCall
&& $expr->right->name instanceof Name
&& in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true)
&& count($expr->right->getArgs()) >= 1
) {
$countFuncCall = $expr->right;
} elseif (
$expr->right instanceof Node\Expr\BinaryOp\Minus
&& $expr->right->left instanceof FuncCall
&& $expr->right->left->name instanceof Name
&& in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true)
&& count($expr->right->left->getArgs()) >= 1
&& $expr->right->right instanceof Node\Scalar\Int_
&& $expr->right->right->value >= 1
) {
$countFuncCall = $expr->right->left;
$subtraction = $expr->right->right->value;
}

if (
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the bot is trying to support thing like

if (3 < count($array) - 1)

!$context->null()
&& $countFuncCall !== null
&& $leftType->isInteger()->yes()
) {
$argType = $scope->getType($expr->right->getArgs()[0]->value);
$argType = $scope->getType($countFuncCall->getArgs()[0]->value);

if ($leftType instanceof ConstantIntegerType) {
if ($orEqual) {
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue());
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue() + $subtraction);
} else {
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue());
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue() + $subtraction);
}
} elseif ($leftType instanceof IntegerRangeType) {
$sizeType = $leftType->shift($offset);
$sizeType = $leftType->shift($offset + $subtraction);
} else {
$sizeType = $leftType;
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we add $substraction here too ?

}

$specifiedTypes = $this->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr);
$specifiedTypes = $this->specifyTypesForCountFuncCall($countFuncCall, $argType, $sizeType, $context, $scope, $expr);
if ($specifiedTypes !== null) {
$result = $result->unionWith($specifiedTypes);
}

if (
$context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes())
|| ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes())
$context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset - $subtraction)->isSuperTypeOf($leftType)->yes())
|| ($context->false() && (new ConstantIntegerType(1 - $offset - $subtraction))->isSuperTypeOf($leftType)->yes())
) {
if ($context->truthy() && $argType->isArray()->maybe()) {
$countables = [];
Expand All @@ -318,7 +338,7 @@ public function specifyTypesInCondition(
if (count($countables) > 0) {
$countableType = TypeCombinator::union(...$countables);

return $this->create($expr->right->getArgs()[0]->value, $countableType, $context, $scope)->setRootExpr($expr);
return $this->create($countFuncCall->getArgs()[0]->value, $countableType, $context, $scope)->setRootExpr($expr);
}
}

Expand All @@ -329,21 +349,21 @@ public function specifyTypesInCondition(
}

$result = $result->unionWith(
$this->create($expr->right->getArgs()[0]->value, $newType, $context, $scope)->setRootExpr($expr),
$this->create($countFuncCall->getArgs()[0]->value, $newType, $context, $scope)->setRootExpr($expr),
);
}
}

// infer $list[$index] after $index < count($list)
// infer $list[$index] after $index < count($list) or $index < count($list) - K
if (
$context->true()
&& !$orEqual
&& (!$orEqual || $subtraction >= 1)
// 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;
$arrayArg = $countFuncCall->getArgs()[0]->value;
$dimFetch = new ArrayDimFetch($arrayArg, $expr->left);
$result = $result->unionWith(
$this->create($dimFetch, $argType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1189,4 +1189,16 @@ public function testBug13770(): void
]);
}

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

$this->analyse([__DIR__ . '/data/bug-14215.php'], [
[
'Offset int might not exist on list<int>.',
39,
],
]);
}

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

namespace Bug14215;

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

return 0;
}

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

return 0;
}

/**
* @param list<int> $array
*/
public function intLessThanOrEqualCountMinusOne(array $array, int $index): int
{
if ($index <= count($array) - 1) {
return $array[$index]; // should error report, could be negative int
}

return 0;
}

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

return 0;
}

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

return 0;
}

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

return 0;
}

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

return 0;
}

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

return 0;
}

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

return 0;
}
}
Loading