Skip to content

Commit d3148e6

Browse files
phpstan-botclaude
andcommitted
Handle optional keys in constant arrays for !in_array narrowing
When computing guaranteed values for the false context of in_array(), exclude values from optional keys in constant arrays since they may not be present at runtime. For array{a: 'a', c: 'c'}|array{a?: 'a', b: 'b'}, the value 'a' from the optional key should not be considered guaranteed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b18deb5 commit d3148e6

File tree

2 files changed

+33
-1
lines changed

2 files changed

+33
-1
lines changed

src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use PHPStan\Type\ArrayType;
1919
use PHPStan\Type\FunctionTypeSpecifyingExtension;
2020
use PHPStan\Type\MixedType;
21+
use PHPStan\Type\NeverType;
2122
use PHPStan\Type\TypeCombinator;
2223
use PHPStan\Type\UnionType;
2324
use function count;
@@ -123,7 +124,26 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
123124
$innerTypes = $arrayType instanceof UnionType ? $arrayType->getTypes() : [$arrayType];
124125
$innerValueTypes = [];
125126
foreach ($innerTypes as $innerType) {
126-
$innerValueTypes[] = $innerType->getIterableValueType();
127+
$constantArrays = $innerType->getConstantArrays();
128+
if (count($constantArrays) > 0) {
129+
// Only include values from non-optional keys, since optional
130+
// keys may not be present in the array at runtime.
131+
$perArrayTypes = [];
132+
foreach ($constantArrays as $constantArray) {
133+
$guaranteedTypes = [];
134+
foreach ($constantArray->getValueTypes() as $i => $valueType) {
135+
if (!$constantArray->isOptionalKey($i)) {
136+
$guaranteedTypes[] = $valueType;
137+
}
138+
}
139+
$perArrayTypes[] = count($guaranteedTypes) > 0
140+
? TypeCombinator::union(...$guaranteedTypes)
141+
: new NeverType();
142+
}
143+
$innerValueTypes[] = TypeCombinator::intersect(...$perArrayTypes);
144+
} else {
145+
$innerValueTypes[] = $innerType->getIterableValueType();
146+
}
127147
}
128148
if (count($innerValueTypes) > 0) {
129149
$narrowingValueType = TypeCombinator::intersect(...$innerValueTypes);

tests/PHPStan/Analyser/nsrt/bug-14407.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,15 @@ function () {
7474
assertType('Bug14407\SomeEnum::A', $x);
7575
}
7676
};
77+
78+
/**
79+
* @param 'a'|'b'|'c' $x
80+
* @param array{a: 'a', c: 'c'}|array{a?:'a', b: 'b'} $a
81+
*/
82+
function testUnionWithOptionalKeys($x, $a): void
83+
{
84+
assertType("array{a: 'a', c: 'c'}|array{a?: 'a', b: 'b'}", $a);
85+
if (!\in_array($x, $a, true)) {
86+
assertType("'a'|'b'|'c'", $x);
87+
}
88+
};

0 commit comments

Comments
 (0)