diff --git a/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php b/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php index e8c1008c183..a59f29cd6d6 100644 --- a/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php +++ b/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php @@ -17,7 +17,6 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\NeverType; use PHPStan\Type\TypeCombinator; -use function array_keys; use function count; use function is_string; @@ -201,10 +200,12 @@ public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $con } $conditions = $conditionExpressionTypes; - foreach (array_keys($conditions) as $conditionExprString) { + $droppedSelfCondition = null; + foreach ($conditions as $conditionExprString => $condition) { if ($conditionExprString !== $exprString) { continue; } + $droppedSelfCondition = $condition; unset($conditions[$conditionExprString]); } @@ -218,6 +219,16 @@ public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $con ? TypeCombinator::intersect($targetType, $type) : TypeCombinator::remove($targetType, $type); + // The dropped self-condition narrowed the target; without it the + // holder must allow the values it excluded, or it over-narrows when + // only the remaining conditions hold. So union back the complement. + if ($droppedSelfCondition !== null) { + $complement = TypeCombinator::remove($scope->getType($expr), $droppedSelfCondition->getType()); + if (!$complement instanceof NeverType) { + $holderType = TypeCombinator::union($holderType, $complement); + } + } + // These boolean-decomposition holders only refine an expression's // type in a future scope; they must never collapse it to never and // thereby mark the whole scope unreachable. A never result is an diff --git a/tests/PHPStan/Analyser/nsrt/bug-14874.php b/tests/PHPStan/Analyser/nsrt/bug-14874.php new file mode 100644 index 00000000000..28506ef3e38 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14874.php @@ -0,0 +1,29 @@ + $a + */ +function test(array $a): bool { + if (isset($a['foo']) && !is_array($a['foo'])) + return false; + if (array_key_exists('foo', $a)) { + assertType('array|null', $a['foo']); + return $a['foo'] === null; // possible + } + return false; +} + +/** + * @param array $a + */ +function testIs(array $a): void { + if (isset($a['foo']) && !is_int($a['foo'])) + return; + if (array_key_exists('foo', $a)) { + assertType('int|null', $a['foo']); + } +}