Thanks to visit codestin.com
Credit goes to github.com

Skip to content
23 changes: 19 additions & 4 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -280,21 +280,36 @@
) {
$argType = $scope->getType($expr->right->getArgs()[0]->value);

$sizeType = null;
if ($leftType instanceof ConstantIntegerType) {
if ($orEqual) {
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue());
} else {
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue());
}
} elseif ($leftType instanceof IntegerRangeType) {
$sizeType = $leftType->shift($offset);
if ($context->falsey() && $leftType->getMax() !== null) {

Check warning on line 291 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrueTruthyFalseFalseyTypeSpecifierContextMutator": @@ @@ $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); } } elseif ($leftType instanceof IntegerRangeType) { - if ($context->falsey() && $leftType->getMax() !== null) { + if ($context->false() && $leftType->getMax() !== null) { if ($orEqual) { $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMax()); } else {

Check warning on line 291 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrueTruthyFalseFalseyTypeSpecifierContextMutator": @@ @@ $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); } } elseif ($leftType instanceof IntegerRangeType) { - if ($context->falsey() && $leftType->getMax() !== null) { + if ($context->false() && $leftType->getMax() !== null) { if ($orEqual) { $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMax()); } else {
if ($orEqual) {
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMax());
} else {
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMax());
}
} elseif ($context->truthy() && $leftType->getMin() !== null) {

Check warning on line 297 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrueTruthyFalseFalseyTypeSpecifierContextMutator": @@ @@ } else { $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMax()); } - } elseif ($context->truthy() && $leftType->getMin() !== null) { + } elseif ($context->true() && $leftType->getMin() !== null) { if ($orEqual) { $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMin()); } else {

Check warning on line 297 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrueTruthyFalseFalseyTypeSpecifierContextMutator": @@ @@ } else { $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMax()); } - } elseif ($context->truthy() && $leftType->getMin() !== null) { + } elseif ($context->true() && $leftType->getMin() !== null) { if ($orEqual) { $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMin()); } else {
if ($orEqual) {
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMin());
} else {
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMin());
}
}
} else {
$sizeType = $leftType;
}

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

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

namespace Bug13705;
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.

the newly added bug-13705.php should also be checked against a rule test which verifies we will no longer get
"Call to function in_array() with arguments literal-string&non-falsy-string, array{} and true will always evaluate to false." and similar errors

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Pushed both commits. Here's a summary of the changes:

Commit 1: Add rule test for bug-13705

  • Added tests/PHPStan/Rules/Comparison/data/bug-13705.php with three test cases (simple while loop, compound condition with $quantity >= 1 &&, and the original issue's class method)
  • Added testBug13705() to ImpossibleCheckTypeFunctionCallRuleTest expecting no errors

Commit 2: Fix IntegerRangeType count narrowing for falsey context with unbounded max

  • The rule test exposed that the original fix was incomplete: when $quantity is int<1, max> (unbounded max, e.g. after $quantity >= 1 narrowing), the falsey context fell through to the min-based branch, producing sizeType = int<1, max> which incorrectly narrowed arrays to array{}
  • Fixed by changing elseif ($leftType->getMin() !== null) to elseif ($context->truthy() && $leftType->getMin() !== null) — the min-based branch should only apply in truthy context
  • When falsey with unbounded max and truthy with unbounded min, $sizeType is left as null and no count narrowing occurs (correct behavior since we can't determine a definite threshold)
  • Updated bug-4700.php test expectations which were previously overly precise (narrowing to array{} when the range was unbounded)


use function PHPStan\Testing\assertType;

function whileLoop(): void
{
$quantity = random_int(1, 42);
$codes = [];
while (count($codes) < $quantity) {
assertType('list<non-empty-string>', $codes);
$code = random_bytes(16);
if (!in_array($code, $codes, true)) {
$codes[] = $code;
}
}
}

/**
* @param list<string> $arr
* @param int<2, 5> $boundedRange
* @param int<2, max> $unboundedMaxRange
* @param int<min, 5> $unboundedMinRange
*/
function countLessThanRange(array $arr, int $boundedRange, int $unboundedMaxRange, int $unboundedMinRange): void
{
// count($arr) < $range → inverted to NOT($range <= count($arr))
// Inner: orEqual=true, false context → falsey + max !== null + orEqual (branch 1)
// Else: orEqual=true, true context → truthy + min !== null + orEqual (branch 3)
if (count($arr) < $boundedRange) {
assertType('list<string>', $arr);
} else {
assertType('non-empty-list<string>&hasOffsetValue(1, string)', $arr);
}

// count($arr) < unbounded max range → falsey + max is null → fallback via min (branch 3/4)
if (count($arr) < $unboundedMaxRange) {
assertType('list<string>', $arr);
} else {
assertType('non-empty-list<string>&hasOffsetValue(1, string)', $arr);
}

// count($arr) < unbounded min range → fallback branch (min is null)
if (count($arr) < $unboundedMinRange) {
assertType('list<string>', $arr);
} else {
assertType('list<string>', $arr);
}
}

/**
* @param list<string> $arr
* @param int<2, 5> $boundedRange
*/
function countLessThanOrEqualRange(array $arr, int $boundedRange): void
{
// count($arr) <= $range → inverted to NOT($range < count($arr))
// Inner: orEqual=false, false context → falsey + max !== null + !orEqual (branch 2)
// Else: orEqual=false, true context → truthy + min !== null + !orEqual (branch 4)
if (count($arr) <= $boundedRange) {
assertType('list<string>', $arr);
} else {
assertType('non-empty-list<string>&hasOffsetValue(1, string)&hasOffsetValue(2, string)', $arr);
}
}

/**
* @param list<string> $arr
* @param int<2, 5> $boundedRange
*/
function rangeGreaterThanOrEqualCount(array $arr, int $boundedRange): void
{
// $range >= count($arr) → same as count($arr) <= $range
if ($boundedRange >= count($arr)) {
assertType('list<string>', $arr);
} else {
assertType('non-empty-list<string>&hasOffsetValue(1, string)&hasOffsetValue(2, string)', $arr);
}
}

/**
* @param list<string> $arr
* @param int<2, 5> $boundedRange
*/
function rangeLessThanOrEqualCount(array $arr, int $boundedRange): void
{
// $range <= count($arr) → direct, orEqual=true
// True context: truthy + orEqual + min !== null (branch 3)
// False context: falsey + orEqual + max !== null (branch 1)
if ($boundedRange <= count($arr)) {
assertType('non-empty-list<string>&hasOffsetValue(1, string)', $arr);
} else {
assertType('list<string>', $arr);
}
}

/**
* @param list<string> $arr
* @param int<2, 5> $boundedRange
*/
function rangeLessThanCount(array $arr, int $boundedRange): void
{
// $range < count($arr) → direct, orEqual=false
// True context: truthy + !orEqual + min !== null (branch 4)
// False context: falsey + !orEqual + max !== null (branch 2)
if ($boundedRange < count($arr)) {
assertType('non-empty-list<string>&hasOffsetValue(1, string)&hasOffsetValue(2, string)', $arr);
} else {
assertType('list<string>', $arr);
}
}

function whileLoopOriginal(int $length, int $quantity): void
{
if ($length < 8) {
throw new \InvalidArgumentException();
}
$codes = [];
while ($quantity >= 1 && count($codes) < $quantity) {
$code = '';
for ($i = 0; $i < $length; $i++) {
$code .= 'x';
}
if (!in_array($code, $codes, true)) {
$codes[] = $code;
}
}
}

class HelloWorld
{
private const MIN_LENGTH = 8;

/**
* @return list<non-empty-string>
*/
public function generatePlainRecoveryCodes(int $length = 8, int $quantity = 8): array
{
if ($length < self::MIN_LENGTH) {
throw new \InvalidArgumentException(
$length . ' is not allowed as length for recovery codes. Must be at least ' . self::MIN_LENGTH,
1613666803
);
}
$codes = [];
while ($quantity >= 1 && count($codes) < $quantity) {
$code = '';
for ($i = 0; $i < $length; $i++) {
$code .= 'x';
}
if (!in_array($code, $codes, true)) {
$codes[] = $code;
}
}
return $codes;
}
}
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/bug-4700.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ function(array $array, int $count): void {
assertType('int<1, 5>', count($a));
assertType('list{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a);
} else {
assertType('0', count($a));
assertType('array{}', $a);
assertType('int<0, 5>', count($a));
assertType('array{}|list{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a);
}
};

Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug11480.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public function intRangeCount($count): void
if (count($x) >= $count) {
assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
} else {
assertType("array{}", $x);
assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
}
assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
}
Expand Down
10 changes: 5 additions & 5 deletions tests/PHPStan/Analyser/nsrt/list-count.php
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ protected function testOptionalKeysInUnionArray($row): void
protected function testOptionalKeysInUnionListWithIntRange($row, $listRow, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven, $threeOrMoreInRangeLimit, $threeOrMoreOverRangeLimit): void
{
if (count($row) >= $twoOrThree) {
assertType('array{0: int, 1: string|null, 2?: int|null}', $row);
assertType('list{0: int, 1: string|null, 2?: int|null, 3?: float|null}', $row);
} else {
assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
}
Expand All @@ -376,25 +376,25 @@ protected function testOptionalKeysInUnionListWithIntRange($row, $listRow, $twoO
}

if (count($row) >= $threeOrMoreInRangeLimit) {
assertType('list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
assertType('array{0: int, 1: string|null, 2: int|null, 3?: float|null}', $row);
} else {
assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
}

if (count($listRow) >= $threeOrMoreInRangeLimit) {
assertType('list{0: string, 1: string, 2: string, 3?: string, 4?: string, 5?: string, 6?: string, 7?: string, 8?: string, 9?: string, 10?: string, 11?: string, 12?: string, 13?: string, 14?: string, 15?: string, 16?: string, 17?: string, 18?: string, 19?: string, 20?: string, 21?: string, 22?: string, 23?: string, 24?: string, 25?: string, 26?: string, 27?: string, 28?: string, 29?: string, 30?: string, 31?: string}', $listRow);
assertType('non-empty-list<string>&hasOffsetValue(1, string)&hasOffsetValue(2, string)', $listRow);
} else {
assertType('list<string>', $listRow);
}

if (count($row) >= $threeOrMoreOverRangeLimit) {
assertType('list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
assertType('array{0: int, 1: string|null, 2: int|null, 3?: float|null}', $row);
} else {
assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
}

if (count($listRow) >= $threeOrMoreOverRangeLimit) {
assertType('non-empty-list<string>', $listRow);
assertType('non-empty-list<string>&hasOffsetValue(1, string)&hasOffsetValue(2, string)', $listRow);
} else {
assertType('list<string>', $listRow);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,12 @@ public function testBug14429(): void
$this->analyse([__DIR__ . '/data/bug-14429.php'], []);
}

public function testBug13705(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13705.php'], []);
}

public function testBug13799(): void
{
$this->treatPhpDocTypesAsCertain = true;
Expand Down
Loading