Skip to content

Commit f99c6ec

Browse files
VincentLangletphpstan-bot
authored andcommitted
Fix phpstan/phpstan#14314: Incorrect count narrowing removes unrelated constant arrays from union
- Added fallback in TypeSpecifier::specifyTypesForCountFuncCall for falsey context - When recursive truthy call fails (remaining size includes 0), directly filter constant arrays by their exact sizes instead of using TypeCombinator::remove - This avoids the issue where remove() considers a shorter constant array as a supertype of a longer one, incorrectly eliminating both from the union - New regression test in tests/PHPStan/Analyser/nsrt/bug-14314.php
1 parent 79e39da commit f99c6ec

2 files changed

Lines changed: 41 additions & 0 deletions

File tree

src/Analyser/TypeSpecifier.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,27 @@ private function specifyTypesForCountFuncCall(
12941294
return $result;
12951295
}
12961296
}
1297+
1298+
// Fallback: directly filter constant arrays by their exact sizes.
1299+
// This avoids using TypeCombinator::remove() with falsey context,
1300+
// which can incorrectly remove arrays whose count doesn't match
1301+
// but whose shape is a subtype of the matched array.
1302+
$keptTypes = [];
1303+
foreach ($type->getConstantArrays() as $arrayType) {
1304+
if ($sizeType->isSuperTypeOf($arrayType->getArraySize())->yes()) {
1305+
continue;
1306+
}
1307+
1308+
$keptTypes[] = $arrayType;
1309+
}
1310+
if ($keptTypes !== []) {
1311+
return $this->create(
1312+
$countFuncCall->getArgs()[0]->value,
1313+
TypeCombinator::union(...$keptTypes),
1314+
$context->negate(),
1315+
$scope,
1316+
)->setRootExpr($rootExpr);
1317+
}
12971318
}
12981319

12991320
$resultTypes = [];
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14314;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
function () {
10+
preg_match('/^(.)$/', '', $matches) || preg_match('/^(.)(.)(.)$/', '', $matches);
11+
assertType('array{}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}|array{non-falsy-string, non-empty-string}', $matches);
12+
if (count($matches) === 2) {
13+
assertType('array{non-falsy-string, non-empty-string}', $matches);
14+
return;
15+
}
16+
assertType('array{}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}', $matches);
17+
if (count($matches) === 4) {
18+
assertType('array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}', $matches);
19+
}
20+
};

0 commit comments

Comments
 (0)