Skip to content

Commit e7941d4

Browse files
phpstan-botstaabm
authored andcommitted
Decompose union offset types in ConstantArrayType::flipArray() and fillKeysArray() to preserve per-key value precision
- In `flipArray()`, when a value type decomposes into multiple constant scalar types (e.g. `'a'|'b'|'c'`), iterate over each scalar individually with `optional=true` instead of passing the full union to `setOffsetValueType`. This prevents the builder's optional-key-replacement logic from overwriting previous values and avoids partial-match degradation to general array types. - Apply the same decomposition in `fillKeysArray()` to fix the analogous partial-match degradation when overlapping union value types are used as keys. - Add `NonEmptyArrayType` wrapping in `ArrayFillKeysFunctionReturnTypeExtension` when the input array is non-empty, mirroring the existing logic in `ArrayFlipFunctionReturnTypeExtension`.
1 parent 2f976a8 commit e7941d4

3 files changed

Lines changed: 59 additions & 5 deletions

File tree

src/Type/Constant/ConstantArrayType.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -969,9 +969,9 @@ public function fillKeysArray(Type $valueType): Type
969969
return $stringKeyType;
970970
}
971971

972-
$builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i));
972+
$builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i) || count($stringKeyType->getConstantScalarTypes()) > 1);
973973
} else {
974-
$builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i));
974+
$builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i) || count($keyType->getConstantScalarTypes()) > 1);
975975
}
976976
}
977977

@@ -984,10 +984,11 @@ public function flipArray(): Type
984984

985985
foreach ($this->keyTypes as $i => $keyType) {
986986
$valueType = $this->valueTypes[$i];
987+
$offsetType = $valueType->toArrayKey();
987988
$builder->setOffsetValueType(
988-
$valueType->toArrayKey(),
989+
$offsetType,
989990
$keyType,
990-
$this->isOptionalKey($i),
991+
$this->isOptionalKey($i) || count($offsetType->getConstantScalarTypes()) > 1,
991992
);
992993
}
993994

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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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'|'b'|'c', 0|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'|'b'|'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+
}

0 commit comments

Comments
 (0)