diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index de3c4179b2..d91b69ec02 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3216,6 +3216,8 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self if (array_key_exists($conditionalExprString, $conditions)) { continue; } + + // Pass 1: exact matching via equals() foreach ($conditionalExpressions as $conditionalExpression) { foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) { @@ -3226,6 +3228,33 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self $conditions[$conditionalExprString][] = $conditionalExpression; $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); } + + // Pass 2: use isSuperTypeOf for condition matching + // This handles cases where the condition type is broader than the + // narrowed type — e.g. 'value1'|'value2' (condition) won't match + // 'value1' (narrowed) via equals(), but should match via + // isSuperTypeOf. Only attempted when pass 1 found no matches, to + // avoid conflicts with broader conditions that have lower certainty + // from scope merging. + if (!array_key_exists($conditionalExprString, $conditions)) { + foreach ($conditionalExpressions as $conditionalExpression) { + foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { + if (!array_key_exists($holderExprString, $specifiedExpressions)) { + continue 2; + } + $specifiedHolder = $specifiedExpressions[$holderExprString]; + if (!$specifiedHolder->getCertainty()->equals($conditionalTypeHolder->getCertainty())) { + continue 2; + } + if (!$conditionalTypeHolder->getType()->isSuperTypeOf($specifiedHolder->getType())->yes()) { + continue 2; + } + } + + $conditions[$conditionalExprString][] = $conditionalExpression; + $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); + } + } } } diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 126f057953..3d4475d593 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1939,9 +1939,15 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes continue; } + $scopeType = $scope->getType($expr); + $conditionType = TypeCombinator::remove($scopeType, $type); + if ($scopeType->equals($conditionType)) { + continue; + } + $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes( $expr, - TypeCombinator::remove($scope->getType($expr), $type), + $conditionType, ); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-10055.php b/tests/PHPStan/Analyser/nsrt/bug-10055.php new file mode 100644 index 0000000000..2464880ffc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10055.php @@ -0,0 +1,32 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug10055; + +use function PHPStan\Testing\assertType; + +/** + * @param 'value1'|'value2'|'value3' $param1 + * @param ($param1 is 'value3' ? bool : int) $param2 + */ +function test(string $param1, int|bool $param2): void +{ + match ($param1) { + 'value1' => assertType('int', $param2), + 'value2' => assertType('int', $param2), + 'value3' => assertType('bool', $param2), + }; +} + +function testScopeMerging(mixed $foo): void +{ + $a = 0; + if (\is_string($foo) || \is_int($foo)) { + $a = 1; + } + + if (\is_int($foo)) { + assertType('1', $a); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5051.php b/tests/PHPStan/Analyser/nsrt/bug-5051.php index 6c3e80dce1..94ffc4711c 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5051.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5051.php @@ -60,27 +60,27 @@ public function testWithBooleans($data): void assertType('bool', $update); } else { assertType('1|2', $data); - assertType('bool', $update); + assertType('false', $update); } if ($data === 1) { - assertType('bool', $update); - assertType('bool', $foo); + assertType('false', $update); + assertType('false', $foo); } else { assertType('bool', $update); assertType('bool', $foo); } if ($data === 2) { - assertType('bool', $update); - assertType('bool', $foo); + assertType('false', $update); + assertType('false', $foo); } else { assertType('bool', $update); assertType('bool', $foo); } if ($data === 3) { - assertType('bool', $update); + assertType('false', $update); assertType('true', $foo); } else { assertType('bool', $update); @@ -88,7 +88,7 @@ public function testWithBooleans($data): void } if ($data === 1 || $data === 2) { - assertType('bool', $update); + assertType('false', $update); assertType('false', $foo); } else { assertType('bool', $update);