Skip to content

Commit 409ead6

Browse files
authored
Decompose union offset types in ConstantArrayType::flipArray() and fillKeysArray() to preserve per-key value precision (#5719)
1 parent 24cbdd8 commit 409ead6

3 files changed

Lines changed: 113 additions & 5 deletions

File tree

src/Type/Constant/ConstantArrayType.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,9 +1513,9 @@ public function fillKeysArray(Type $valueType): Type
15131513
return $stringKeyType;
15141514
}
15151515

1516-
$builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i));
1516+
$builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i) || count($stringKeyType->getConstantScalarTypes()) > 1);
15171517
} else {
1518-
$builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i));
1518+
$builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i) || count($keyType->getConstantScalarTypes()) > 1);
15191519
}
15201520
}
15211521

@@ -1541,10 +1541,11 @@ public function flipArray(): Type
15411541

15421542
foreach ($this->keyTypes as $i => $keyType) {
15431543
$valueType = $this->valueTypes[$i];
1544+
$offsetType = $valueType->toArrayKey();
15441545
$builder->setOffsetValueType(
1545-
$valueType->toArrayKey(),
1546+
$offsetType,
15461547
$keyType,
1547-
$this->isOptionalKey($i),
1548+
$this->isOptionalKey($i) || count($offsetType->getConstantScalarTypes()) > 1,
15481549
);
15491550
}
15501551

src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
use PHPStan\DependencyInjection\AutowiredService;
88
use PHPStan\Php\PhpVersion;
99
use PHPStan\Reflection\FunctionReflection;
10+
use PHPStan\Type\Accessory\NonEmptyArrayType;
1011
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1112
use PHPStan\Type\NeverType;
1213
use PHPStan\Type\NullType;
1314
use PHPStan\Type\Type;
15+
use PHPStan\Type\TypeCombinator;
1416
use function count;
1517

1618
#[AutowiredService]
@@ -38,7 +40,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3840
return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType();
3941
}
4042

41-
return $keysType->fillKeysArray($scope->getType($args[1]->value));
43+
$filled = $keysType->fillKeysArray($scope->getType($args[1]->value));
44+
if ($keysType->isIterableAtLeastOnce()->yes() && $filled->isArray()->yes()) {
45+
return TypeCombinator::intersect($filled, new NonEmptyArrayType());
46+
}
47+
return $filled;
4248
}
4349

4450
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14656;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class ArrayFlipUnionValues
8+
{
9+
/** @param array{0: 'a'|'b'|'c', 1: 'a'|'b'|'c', 2: 'a'|'b'|'c'} $a */
10+
public function allUnion(array $a): void
11+
{
12+
assertType("non-empty-array{a?: 0|1|2, b?: 0|1|2, c?: 0|1|2}", array_flip($a));
13+
}
14+
15+
/** @param array{0: 'a'|'b', 1: 'b'|'c'} $a */
16+
public function overlappingUnion(array $a): void
17+
{
18+
assertType("non-empty-array{a?: 0, b?: 0|1, c?: 1}", array_flip($a));
19+
}
20+
21+
/** @param array{0: 'a'|'b', 1: 'c'} $a */
22+
public function mixedUnionAndConstant(array $a): void
23+
{
24+
assertType("array{a?: 0, b?: 0, c: 1}", array_flip($a));
25+
}
26+
}
27+
28+
class ArrayFillKeysUnionValues
29+
{
30+
/** @param array{0: 'a'|'b', 1: 'b'|'c'} $a */
31+
public function overlappingUnion(array $a): void
32+
{
33+
assertType("non-empty-array{a?: 'x', b?: 'x', c?: 'x'}", array_fill_keys($a, 'x'));
34+
}
35+
36+
/** @param array{0: 'a'|'b'|'c', 1: 'a'|'b'|'c', 2: 'a'|'b'|'c'} $a */
37+
public function allUnion(array $a): void
38+
{
39+
assertType("non-empty-array{a?: 'x', b?: 'x', c?: 'x'}", array_fill_keys($a, 'x'));
40+
}
41+
42+
/** @param array{0: 'a'|'b', 1: 'c'} $a */
43+
public function mixedUnionAndConstant(array $a): void
44+
{
45+
assertType("array{a?: 'x', b?: 'x', c: 'x'}", array_fill_keys($a, 'x'));
46+
}
47+
}
48+
49+
class ArrayFlipUnsealedUnionValues
50+
{
51+
/** @param array{0: 'a'|'b'|'c', 1: 'a'|'b'|'c', 2: 'a'|'b'|'c', ...<int, 'a'|'b'|'c'>} $a */
52+
public function allUnion(array $a): void
53+
{
54+
assertType("non-empty-array{a?: int, b?: int, c?: int}", array_flip($a));
55+
}
56+
57+
/** @param array{0: 'a'|'b', 1: 'b'|'c', ...<int, 'a'|'b'|'c'>} $a */
58+
public function overlappingUnion(array $a): void
59+
{
60+
assertType("non-empty-array{a?: int, b?: int, c?: int}", array_flip($a));
61+
}
62+
63+
/** @param array{0: 'a'|'b', 1: 'c', ...<int, 'a'|'b'>} $a */
64+
public function mixedUnionAndConstant(array $a): void
65+
{
66+
assertType("array{a?: int, b?: int, c: 1}", array_flip($a));
67+
}
68+
69+
/** @param array{0: 'a'|'b', 1: 'c', ...<int, string>} $a */
70+
public function nonFiniteTail(array $a): void
71+
{
72+
assertType("array{a?: int, b?: int, c: int, ...<string, int>}", array_flip($a));
73+
}
74+
}
75+
76+
class ArrayFillKeysUnsealedUnionValues
77+
{
78+
/** @param array{0: 'a'|'b', 1: 'b'|'c', ...<int, 'a'|'b'|'c'>} $a */
79+
public function overlappingUnion(array $a): void
80+
{
81+
assertType("non-empty-array{a?: 'x', b?: 'x', c?: 'x'}", array_fill_keys($a, 'x'));
82+
}
83+
84+
/** @param array{0: 'a'|'b'|'c', 1: 'a'|'b'|'c', 2: 'a'|'b'|'c', ...<int, 'a'|'b'|'c'>} $a */
85+
public function allUnion(array $a): void
86+
{
87+
assertType("non-empty-array{a?: 'x', b?: 'x', c?: 'x'}", array_fill_keys($a, 'x'));
88+
}
89+
90+
/** @param array{0: 'a'|'b', 1: 'c', ...<int, 'a'|'b'>} $a */
91+
public function mixedUnionAndConstant(array $a): void
92+
{
93+
assertType("array{a?: 'x', b?: 'x', c: 'x'}", array_fill_keys($a, 'x'));
94+
}
95+
96+
/** @param array{0: 'a'|'b', 1: 'c', ...<int, string>} $a */
97+
public function nonFiniteTail(array $a): void
98+
{
99+
assertType("array{a?: 'x', b?: 'x', c: 'x', ...<string, 'x'>}", array_fill_keys($a, 'x'));
100+
}
101+
}

0 commit comments

Comments
 (0)