Skip to content

Commit bd08780

Browse files
VincentLangletphpstan-bot
authored andcommitted
Fix count comparison with IntegerRangeType narrowing array too aggressively
- Fixed TypeSpecifier to properly compute sizeType for count() comparisons when the compared value is an IntegerRangeType (e.g. int<1, 42>) - In falsey context, use max of range to avoid incorrectly removing arrays whose count falls within the range but can still satisfy the condition - In truthy context, use min of range for correct minimum count constraint - Updated test expectations in bug11480.php and list-count.php to reflect the more correct narrowing behavior - New regression test in tests/PHPStan/Analyser/nsrt/bug-13705.php
1 parent 06ea1e1 commit bd08780

File tree

4 files changed

+56
-7
lines changed

4 files changed

+56
-7
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,21 @@ public function specifyTypesInCondition(
287287
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue());
288288
}
289289
} elseif ($leftType instanceof IntegerRangeType) {
290-
$sizeType = $leftType->shift($offset);
290+
if ($context->falsey() && $leftType->getMax() !== null) {
291+
if ($orEqual) {
292+
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMax());
293+
} else {
294+
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMax());
295+
}
296+
} elseif ($leftType->getMin() !== null) {
297+
if ($orEqual) {
298+
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMin());
299+
} else {
300+
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMin());
301+
}
302+
} else {
303+
$sizeType = $leftType->shift($offset);
304+
}
291305
} else {
292306
$sizeType = $leftType;
293307
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13705;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function whileLoop(): void
8+
{
9+
$quantity = random_int(1, 42);
10+
$codes = [];
11+
while (count($codes) < $quantity) {
12+
assertType('list<non-empty-string>', $codes);
13+
$code = random_bytes(16);
14+
if (!in_array($code, $codes, true)) {
15+
$codes[] = $code;
16+
}
17+
}
18+
}
19+
20+
function whileLoopOriginal(int $length, int $quantity): void
21+
{
22+
if ($length < 8) {
23+
throw new \InvalidArgumentException();
24+
}
25+
$codes = [];
26+
while ($quantity >= 1 && count($codes) < $quantity) {
27+
$code = '';
28+
for ($i = 0; $i < $length; $i++) {
29+
$code .= 'x';
30+
}
31+
if (!in_array($code, $codes, true)) {
32+
$codes[] = $code;
33+
}
34+
}
35+
}

tests/PHPStan/Analyser/nsrt/bug11480.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public function intRangeCount($count): void
106106
if (count($x) >= $count) {
107107
assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
108108
} else {
109-
assertType("array{}", $x);
109+
assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
110110
}
111111
assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
112112
}

tests/PHPStan/Analyser/nsrt/list-count.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ protected function testOptionalKeysInUnionArray($row): void
352352
protected function testOptionalKeysInUnionListWithIntRange($row, $listRow, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven, $threeOrMoreInRangeLimit, $threeOrMoreOverRangeLimit): void
353353
{
354354
if (count($row) >= $twoOrThree) {
355-
assertType('array{0: int, 1: string|null, 2?: int|null}', $row);
355+
assertType('list{0: int, 1: string|null, 2?: int|null, 3?: float|null}', $row);
356356
} else {
357357
assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
358358
}
@@ -376,25 +376,25 @@ protected function testOptionalKeysInUnionListWithIntRange($row, $listRow, $twoO
376376
}
377377

378378
if (count($row) >= $threeOrMoreInRangeLimit) {
379-
assertType('list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
379+
assertType('array{0: int, 1: string|null, 2: int|null, 3?: float|null}', $row);
380380
} else {
381381
assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
382382
}
383383

384384
if (count($listRow) >= $threeOrMoreInRangeLimit) {
385-
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);
385+
assertType('non-empty-list<string>&hasOffsetValue(1, string)&hasOffsetValue(2, string)', $listRow);
386386
} else {
387387
assertType('list<string>', $listRow);
388388
}
389389

390390
if (count($row) >= $threeOrMoreOverRangeLimit) {
391-
assertType('list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
391+
assertType('array{0: int, 1: string|null, 2: int|null, 3?: float|null}', $row);
392392
} else {
393393
assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
394394
}
395395

396396
if (count($listRow) >= $threeOrMoreOverRangeLimit) {
397-
assertType('non-empty-list<string>', $listRow);
397+
assertType('non-empty-list<string>&hasOffsetValue(1, string)&hasOffsetValue(2, string)', $listRow);
398398
} else {
399399
assertType('list<string>', $listRow);
400400
}

0 commit comments

Comments
 (0)