Skip to content
16 changes: 15 additions & 1 deletion src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,21 @@
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue());
}
} elseif ($leftType instanceof IntegerRangeType) {
$sizeType = $leftType->shift($offset);
if ($context->falsey() && $leftType->getMax() !== null) {

Check warning on line 290 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 {

Check warning on line 290 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 {
if ($orEqual) {
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMax());
} else {
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMax());
}
} elseif ($leftType->getMin() !== null) {
if ($orEqual) {
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMin());
} else {
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMin());
}
} else {
$sizeType = $leftType->shift($offset);
}
} else {
$sizeType = $leftType;
}
Expand Down
35 changes: 35 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13705.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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;
}
}
}

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;
}
}
}
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
Loading