Skip to content

Commit 7f296d9

Browse files
VincentLangletphpstan-bot
authored andcommitted
Fix count-based type narrowing for constant array unions
- Fixed specifyTypesForCountFuncCall to use direct type assignment instead of TypeCombinator::remove for falsey context with fixed-size constant arrays - The root cause was ConstantArrayType::isSuperTypeOf using structural subtyping (array{mixed} is supertype of array{mixed, string|null, mixed}), which made TypeCombinator::remove overly aggressive - New regression test in tests/PHPStan/Analyser/nsrt/bug-11488.php Closes phpstan/phpstan#11488
1 parent 099a0ba commit 7f296d9

File tree

2 files changed

+67
-0
lines changed

2 files changed

+67
-0
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,13 +1272,20 @@ private function specifyTypesForCountFuncCall(
12721272
}
12731273

12741274
$resultTypes = [];
1275+
$nonMatchingTypes = [];
1276+
$allArraysHaveDefiniteSize = true;
12751277
foreach ($type->getArrays() as $arrayType) {
1278+
if (!$arrayType->getArraySize()->isConstantScalarValue()->yes()) {
1279+
$allArraysHaveDefiniteSize = false;
1280+
}
12761281
$isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($arrayType->getArraySize());
12771282
if ($isSizeSuperTypeOfArraySize->no()) {
1283+
$nonMatchingTypes[] = $arrayType;
12781284
continue;
12791285
}
12801286

12811287
if ($context->falsey() && $isSizeSuperTypeOfArraySize->maybe()) {
1288+
$nonMatchingTypes[] = $arrayType;
12821289
continue;
12831290
}
12841291

@@ -1371,6 +1378,13 @@ private function specifyTypesForCountFuncCall(
13711378
$resultTypes[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
13721379
}
13731380

1381+
if ($context->falsey() && $isConstantArray->yes() && $allArraysHaveDefiniteSize) {
1382+
if ($nonMatchingTypes === []) {
1383+
return $this->create($countFuncCall->getArgs()[0]->value, new NeverType(), TypeSpecifierContext::createTrue(), $scope)->setAlwaysOverwriteTypes()->setRootExpr($rootExpr);
1384+
}
1385+
return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$nonMatchingTypes), TypeSpecifierContext::createTrue(), $scope)->setAlwaysOverwriteTypes()->setRootExpr($rootExpr);
1386+
}
1387+
13741388
return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$resultTypes), $context, $scope)->setRootExpr($rootExpr);
13751389
}
13761390

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug11488;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
/**
10+
* @param array{mixed}|array{mixed, string|null, mixed} $row
11+
*/
12+
protected function test(array $row): string
13+
{
14+
if (count($row) !== 1) {
15+
assertType('array{mixed, string|null, mixed}', $row);
16+
17+
[$field, $operator, $value] = $row;
18+
assertType('string|null', $operator);
19+
return $operator ?? '=';
20+
}
21+
22+
return '';
23+
}
24+
25+
/**
26+
* @param array{bool}|array{mixed, string|null, mixed} $row
27+
*/
28+
protected function test2(array $row): string
29+
{
30+
if (count($row) !== 1) {
31+
assertType('array{mixed, string|null, mixed}', $row);
32+
33+
[$field, $operator, $value] = $row;
34+
assertType('string|null', $operator);
35+
return $operator ?? '=';
36+
}
37+
38+
return '';
39+
}
40+
41+
/**
42+
* @param array{mixed}|array{mixed, string|null, mixed} $row
43+
*/
44+
protected function test3(array $row): string
45+
{
46+
if (count($row) === 3) {
47+
assertType('array{mixed, string|null, mixed}', $row);
48+
return '';
49+
}
50+
51+
return '';
52+
}
53+
}

0 commit comments

Comments
 (0)