Skip to content

Commit 53fa52c

Browse files
phpstan-botclaude
andcommitted
Preserve non-empty array guarantee in ConstantArrayType::setOffsetValueType when union key expansion produces all-optional keys
- `ConstantArrayTypeBuilder` now expands union scalar keys (e.g. `'a'|'b'`) into individual optional entries even for empty arrays (since 2f66c45). Each key is optional because we don't know which branch of the union will be taken, but at least one WILL be set, so the result must be non-empty. - `ArrayType::setOffsetValueType` already intersects with `NonEmptyArrayType`, but `ConstantArrayType::setOffsetValueType` did not, causing the array shape `array{a?: 1, b?: 1}` to be reported as possibly empty. - Add `NonEmptyArrayType` intersection in `ConstantArrayType::setOffsetValueType` when the builder result's `isIterableAtLeastOnce()` is not `yes`. - Update pre-existing test that asserted the buggy (possibly-empty) behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2f627eb commit 53fa52c

3 files changed

Lines changed: 81 additions & 2 deletions

File tree

src/Type/Constant/ConstantArrayType.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,12 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
748748
$builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
749749
$builder->setOffsetValueType($offsetType, $valueType);
750750

751-
return $builder->getArray();
751+
$array = $builder->getArray();
752+
if (!$array->isIterableAtLeastOnce()->yes()) {
753+
$array = TypeCombinator::intersect($array, new NonEmptyArrayType());
754+
}
755+
756+
return $array;
752757
}
753758

754759
public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14552;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @param non-empty-list<'a'|'b'> $keys
9+
*/
10+
function nonEmptyListForeach(array $keys): void
11+
{
12+
$out = [];
13+
foreach ($keys as $k) {
14+
$out[$k] = 1;
15+
}
16+
assertType("non-empty-array{a?: 1, b?: 1}", $out);
17+
}
18+
19+
/**
20+
* @param list<'a'|'b'> $keys
21+
*/
22+
function possiblyEmptyListForeach(array $keys): void
23+
{
24+
$out = [];
25+
foreach ($keys as $k) {
26+
$out[$k] = 1;
27+
}
28+
assertType("array{}|array{a?: 1, b?: 1}", $out);
29+
}
30+
31+
/**
32+
* @param non-empty-list<'x'|'y'|'z'> $keys
33+
*/
34+
function nonEmptyListThreeKeys(array $keys): void
35+
{
36+
$out = [];
37+
foreach ($keys as $k) {
38+
$out[$k] = true;
39+
}
40+
assertType("non-empty-array{x?: true, y?: true, z?: true}", $out);
41+
}
42+
43+
/**
44+
* Direct assignment (non-foreach) with union key on empty array.
45+
* @param 'a'|'b' $key
46+
*/
47+
function directAssignment(string $key): void
48+
{
49+
$arr = [];
50+
$arr[$key] = 1;
51+
assertType("non-empty-array{a?: 1, b?: 1}", $arr);
52+
}
53+
54+
/**
55+
* Direct assignment with integer union key on empty array.
56+
* @param 0|1|2 $key
57+
*/
58+
function directAssignmentIntKey(int $key): void
59+
{
60+
$arr = [];
61+
$arr[$key] = 'val';
62+
assertType("non-empty-array{0?: 'val', 1?: 'val', 2?: 'val'}", $arr);
63+
}
64+
65+
/**
66+
* Setting union key on already non-empty array should stay non-empty.
67+
* @param 'x'|'y' $key
68+
*/
69+
function setOnNonEmptyArray(string $key): void
70+
{
71+
$arr = ['existing' => 0];
72+
$arr[$key] = 1;
73+
assertType("array{existing: 0, x?: 1, y?: 1}", $arr);
74+
}

tests/PHPStan/Analyser/nsrt/set-union-offset-preserve-constant-array.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public function doFoo(): void
1313

1414
$k = rand(0, 1) ? 1 : 2;
1515
$a[$k] = true;
16-
assertType('array{1?: true, 2?: true}', $a);
16+
assertType('non-empty-array{1?: true, 2?: true}', $a);
1717
}
1818

1919
}

0 commit comments

Comments
 (0)