Skip to content

Commit b7ec800

Browse files
staabmondrejmirtes
andauthored
Narrow tagged unions based on count() with array size
Co-authored-by: Ondrej Mirtes <[email protected]>
1 parent ed6bc0b commit b7ec800

File tree

2 files changed

+146
-17
lines changed

2 files changed

+146
-17
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -984,31 +984,49 @@ private function specifyTypesForConstantBinaryExpression(
984984
&& in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true)
985985
&& $constantType instanceof ConstantIntegerType
986986
) {
987+
$argType = $scope->getType($exprNode->getArgs()[0]->value);
988+
989+
if (count($exprNode->getArgs()) === 1) {
990+
$isNormalCount = TrinaryLogic::createYes();
991+
} else {
992+
$mode = $scope->getType($exprNode->getArgs()[1]->value);
993+
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate());
994+
}
995+
996+
if (
997+
$isNormalCount->yes()
998+
&& $argType instanceof UnionType
999+
) {
1000+
$result = [];
1001+
foreach ($argType->getTypes() as $innerType) {
1002+
$arraySize = $innerType->getArraySize();
1003+
$isSize = $constantType->isSuperTypeOf($arraySize);
1004+
if ($context->truthy()) {
1005+
if ($isSize->no()) {
1006+
continue;
1007+
}
1008+
}
1009+
if ($context->falsey()) {
1010+
if (!$isSize->yes()) {
1011+
continue;
1012+
}
1013+
}
1014+
1015+
$result[] = $innerType;
1016+
}
1017+
1018+
return $this->create($exprNode->getArgs()[0]->value, TypeCombinator::union(...$result), $context, false, $scope, $rootExpr);
1019+
}
1020+
9871021
if ($context->truthy() || $constantType->getValue() === 0) {
9881022
$newContext = $context;
9891023
if ($constantType->getValue() === 0) {
9901024
$newContext = $newContext->negate();
9911025
}
9921026

993-
$argType = $scope->getType($exprNode->getArgs()[0]->value);
994-
9951027
if ($argType->isArray()->yes()) {
996-
if (count($exprNode->getArgs()) === 1) {
997-
$isNormalCount = true;
998-
} else {
999-
$mode = $scope->getType($exprNode->getArgs()[1]->value);
1000-
if (!$mode->isInteger()->yes()) {
1001-
return new SpecifiedTypes();
1002-
}
1003-
1004-
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->yes();
1005-
if (!$isNormalCount) {
1006-
$isNormalCount = $argType->getIterableValueType()->isArray()->no();
1007-
}
1008-
}
1009-
10101028
$funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr);
1011-
if ($isNormalCount && $argType->isList()->yes() && $context->truthy() && $constantType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1029+
if ($isNormalCount->yes() && $argType->isList()->yes() && $context->truthy() && $constantType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
10121030
$valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
10131031
$itemType = $argType->getIterableValueType();
10141032
for ($i = 0; $i < $constantType->getValue(); $i++) {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace NarrowTaggedUnion;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
/** @param array{string, '', non-empty-string}|array{string, numeric-string} $arr */
10+
public function sayHello(array $arr): void
11+
{
12+
if (count($arr) === 0) {
13+
assertType('*NEVER*', $arr);
14+
} else {
15+
assertType("array{string, '', non-empty-string}|array{string, numeric-string}", $arr);
16+
}
17+
assertType("array{string, '', non-empty-string}|array{string, numeric-string}", $arr);
18+
19+
if (count($arr) === 1) {
20+
assertType('*NEVER*', $arr);
21+
} else {
22+
assertType("array{string, '', non-empty-string}|array{string, numeric-string}", $arr);
23+
}
24+
assertType("array{string, '', non-empty-string}|array{string, numeric-string}", $arr);
25+
26+
if (count($arr) === 2) {
27+
assertType('array{string, numeric-string}', $arr);
28+
} else {
29+
assertType("array{string, '', non-empty-string}", $arr);
30+
}
31+
assertType("array{string, '', non-empty-string}|array{string, numeric-string}", $arr);
32+
33+
if (count($arr) === 3) {
34+
assertType("array{string, '', non-empty-string}", $arr);
35+
} else {
36+
assertType('array{string, numeric-string}', $arr);
37+
}
38+
assertType("array{string, '', non-empty-string}|array{string, numeric-string}", $arr);
39+
40+
if (count($arr, COUNT_NORMAL) === 3) {
41+
assertType("array{string, '', non-empty-string}", $arr);
42+
} else {
43+
assertType('array{string, numeric-string}', $arr);
44+
}
45+
assertType("array{string, '', non-empty-string}|array{string, numeric-string}", $arr);
46+
47+
if (count($arr, COUNT_RECURSIVE) === 3) {
48+
assertType("array{string, '', non-empty-string}", $arr);
49+
} else {
50+
assertType('array{string, numeric-string}', $arr);
51+
}
52+
assertType("array{string, '', non-empty-string}|array{string, numeric-string}", $arr);
53+
54+
if (count($arr) === 4) {
55+
assertType('*NEVER*', $arr);
56+
} else {
57+
assertType("array{string, '', non-empty-string}|array{string, numeric-string}", $arr);
58+
}
59+
assertType("array{string, '', non-empty-string}|array{string, numeric-string}", $arr);
60+
61+
}
62+
63+
/** @param array{string, '', non-empty-string}|array{array<int>, numeric-string} $arr */
64+
public function nestedArrays(array $arr): void
65+
{
66+
// don't narrow when $arr contains recursive arrays
67+
if (count($arr, COUNT_RECURSIVE) === 3) {
68+
assertType("array{array<int>, numeric-string}|array{string, '', non-empty-string}", $arr);
69+
} else {
70+
assertType("array{array<int>, numeric-string}|array{string, '', non-empty-string}", $arr);
71+
}
72+
assertType("array{array<int>, numeric-string}|array{string, '', non-empty-string}", $arr);
73+
74+
if (count($arr, COUNT_NORMAL) === 3) {
75+
assertType("array{string, '', non-empty-string}", $arr);
76+
} else {
77+
assertType("array{array<int>, numeric-string}", $arr);
78+
}
79+
assertType("array{array<int>, numeric-string}|array{string, '', non-empty-string}", $arr);
80+
}
81+
82+
/** @param array{string, '', non-empty-string}|array<int> $arr */
83+
public function mixedArrays(array $arr): void
84+
{
85+
if (count($arr, COUNT_NORMAL) === 3) {
86+
assertType("non-empty-array<int|string>", $arr); // could be array{string, '', non-empty-string}|non-empty-array<int>
87+
} else {
88+
assertType("array<int|string>", $arr); // could be array{string, '', non-empty-string}|array<int>
89+
}
90+
assertType("array<int|string>", $arr); // could be array{string, '', non-empty-string}|array<int>
91+
}
92+
93+
public function arrayIntRangeSize(): void
94+
{
95+
$x = [];
96+
if (rand(0,1)) {
97+
$x[] = 'ab';
98+
}
99+
if (rand(0,1)) {
100+
$x[] = 'xy';
101+
}
102+
103+
if (count($x) === 1) {
104+
assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
105+
} else {
106+
assertType("array{}|array{0: 'ab', 1?: 'xy'}", $x);
107+
}
108+
assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
109+
}
110+
}
111+

0 commit comments

Comments
 (0)