Do not match conditional expression guards when ConstantArrayType has extra keys#5640
Do not match conditional expression guards when ConstantArrayType has extra keys#5640phpstan-bot wants to merge 2 commits into
Conversation
… extra keys In filterBySpecifiedTypes Pass 2 (supertype match), isSuperTypeOf with structural subtyping for ConstantArrayType allowed a current type with extra keys to satisfy a guard expecting fewer keys. This caused incorrect type narrowing through a chain of conditional expressions. Closes phpstan/phpstan#14595
There was a problem hiding this comment.
This is not related to extra key because this can be reproduced with
/**
* @param array<mixed> $data
* @param array{
* multiple: 0|1|2
* } $options
*/
function formulaire_edition(array $data, array $options, bool $total): void {
$instructions = [ ];
$instructions[] = "foo";
if ($options['multiple'] != 1 || $total) {
$instructions[] = "bar";
}
\PHPStan\dumpType($options['multiple']); // 0|1|2 (ok)
if (!$total) {
$instructions[] = "baz";
}
\PHPStan\dumpType($options['multiple']); // 0|1|2 (ok)
if (!$total) {
$instructions[] = "qux";
}
\PHPStan\dumpType($options['multiple']); // should be: 0|1|2
}
But notice
/**
* @param array<mixed> $data
* @param 0|1|2 $options
*/
function formulaire_edition(array $data, int $options, bool $total): void {
$instructions = [ ];
$instructions[] = "foo";
if ($options != 1 || $total) {
$instructions[] = "bar";
}
\PHPStan\dumpType($options); // 0|1|2 (ok)
if (!$total) {
$instructions[] = "baz";
}
\PHPStan\dumpType($options); // 0|1|2 (ok)
if (!$total) {
$instructions[] = "qux";
}
\PHPStan\dumpType($options); // should be: 0|1|2
}
works fine
The bug also reproduces when `total` is a separate `bool` parameter instead of a key in the `$options` array. The existing fix handles this case because the extra-keys check applies to `$instructions` (the guard condition variable that grows), not to `$options`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Pushed. Here's a summary of the investigation and response to the review: The reviewer's observation is correct — the bug reproduces with their variant where The reason: the "extra keys" check in the fix applies to I verified this by:
I added the reviewer's variant as an additional regression test to cover this scenario explicitly. |
Closes phpstan/phpstan#14595
Summary
When a function had multiple
ifblocks that conditionally appended to an array, PHPStan could incorrectly narrow an unrelated array offset type. The issue required: (1) array offsets accessed from a typed array parameter, (2) conditional logic creating type guards, and (3) multipleifblocks that grew a local array variable.Root cause
In
MutatingScope::filterBySpecifiedTypesPass 2 (supertype match), the guard-matching logic usedisSuperTypeOfto check whether the current type of a variable satisfies a conditional expression guard. ForConstantArrayType,isSuperTypeOfuses structural subtyping —array{0: 'foo'}->isSuperTypeOf(array{0: 'foo', 1: 'bar'})returnsYesbecause the subtype has all required keys. This is correct for method signatures and type acceptance, but too permissive for guard matching: a 2-element array should not match a guard expecting a 1-element array.This caused a chain reaction: narrowing
$optionsto a specific shape fired a conditional expression that narrowed$instructions, and then a stale guard on$instructions(with fewer keys than the actual type) incorrectly fired, narrowing$options['multiple']from0|1|2to1.Fix
After the existing
isSuperTypeOfcheck in Pass 2, add a targeted check forConstantArrayTypeguards: verify that the current type's constant arrays do not have keys absent from the guard. If any current array has a key that the guard array lacks, skip the conditional expression.This preserves structural subtyping for all other contexts (
accepts, method signatures, generalisSuperTypeOfusage) while preventing incorrect guard matching.Test plan
tests/PHPStan/Analyser/nsrt/bug-14595.phpreproduces the exact issue from the bug reportmake phpstanpasses (0 errors)make cs-fixapplied (early-exit style)