From e915e58c28b690b3b0c32b2c2c42a242bb4f4899 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:09:20 +0000 Subject: [PATCH 1/5] Fix conditional parameter type narrowing for union types with 3+ members - Split union condition types into individual ConditionalExpressionHolders - When TypeCombinator::intersect or ::remove produces a UnionType, each member gets its own holder so the equals() check can match individual constant types - New regression test in tests/PHPStan/Analyser/nsrt/bug-10055.php --- src/Analyser/MutatingScope.php | 27 +++++++++++++++-------- tests/PHPStan/Analyser/nsrt/bug-10055.php | 20 +++++++++++++++++ 2 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-10055.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index de3c4179b22..39ec20a2813 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1758,15 +1758,24 @@ private function enterFunctionLike( $ifType = $parameterType->isNegated() ? $parameterType->getElse() : $parameterType->getIf(); $elseType = $parameterType->isNegated() ? $parameterType->getIf() : $parameterType->getElse(); - $holder = new ConditionalExpressionHolder([ - $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::intersect($targetParameter->getType(), $parameterType->getTarget())), - ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $ifType)); - $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; - - $holder = new ConditionalExpressionHolder([ - $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::remove($targetParameter->getType(), $parameterType->getTarget())), - ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $elseType)); - $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; + $ifConditionType = TypeCombinator::intersect($targetParameter->getType(), $parameterType->getTarget()); + $elseConditionType = TypeCombinator::remove($targetParameter->getType(), $parameterType->getTarget()); + + $ifConditionTypes = $ifConditionType instanceof UnionType ? $ifConditionType->getTypes() : [$ifConditionType]; + foreach ($ifConditionTypes as $conditionType) { + $holder = new ConditionalExpressionHolder([ + $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), $conditionType), + ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $ifType)); + $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; + } + + $elseConditionTypes = $elseConditionType instanceof UnionType ? $elseConditionType->getTypes() : [$elseConditionType]; + foreach ($elseConditionTypes as $conditionType) { + $holder = new ConditionalExpressionHolder([ + $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), $conditionType), + ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $elseType)); + $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; + } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-10055.php b/tests/PHPStan/Analyser/nsrt/bug-10055.php new file mode 100644 index 00000000000..8dcfe7ecf18 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10055.php @@ -0,0 +1,20 @@ += 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), + }; +} From 55c57eb6491d792bdea54183b3746764271fff1e Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 11 Apr 2026 11:25:46 +0000 Subject: [PATCH 2/5] Use isSuperTypeOf for condition matching in conditional parameter types Instead of splitting union condition types into individual ConditionalExpressionHolder instances, use isSuperTypeOf at the matching site so that a narrowed type (e.g. 'value1') correctly matches a broader condition type (e.g. 'value1'|'value2'). A new useSubtypeForConditionMatching flag on ConditionalExpressionHolder limits this relaxed matching to conditional parameter type holders only. Other holders (from scope merging, assignments, type specifying) keep strict equals() matching to avoid cascading side effects. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ConditionalExpressionHolder.php | 6 +++ src/Analyser/MutatingScope.php | 42 +++++++++++--------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/Analyser/ConditionalExpressionHolder.php b/src/Analyser/ConditionalExpressionHolder.php index 69071839660..7e2fc68a039 100644 --- a/src/Analyser/ConditionalExpressionHolder.php +++ b/src/Analyser/ConditionalExpressionHolder.php @@ -17,6 +17,7 @@ final class ConditionalExpressionHolder public function __construct( private array $conditionExpressionTypeHolders, private ExpressionTypeHolder $typeHolder, + private bool $useSubtypeForConditionMatching = false, ) { if (count($conditionExpressionTypeHolders) === 0) { @@ -37,6 +38,11 @@ public function getTypeHolder(): ExpressionTypeHolder return $this->typeHolder; } + public function useSubtypeForConditionMatching(): bool + { + return $this->useSubtypeForConditionMatching; + } + public function getKey(): string { $parts = []; diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 39ec20a2813..9a8ac92527f 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1758,24 +1758,15 @@ private function enterFunctionLike( $ifType = $parameterType->isNegated() ? $parameterType->getElse() : $parameterType->getIf(); $elseType = $parameterType->isNegated() ? $parameterType->getIf() : $parameterType->getElse(); - $ifConditionType = TypeCombinator::intersect($targetParameter->getType(), $parameterType->getTarget()); - $elseConditionType = TypeCombinator::remove($targetParameter->getType(), $parameterType->getTarget()); - - $ifConditionTypes = $ifConditionType instanceof UnionType ? $ifConditionType->getTypes() : [$ifConditionType]; - foreach ($ifConditionTypes as $conditionType) { - $holder = new ConditionalExpressionHolder([ - $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), $conditionType), - ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $ifType)); - $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; - } - - $elseConditionTypes = $elseConditionType instanceof UnionType ? $elseConditionType->getTypes() : [$elseConditionType]; - foreach ($elseConditionTypes as $conditionType) { - $holder = new ConditionalExpressionHolder([ - $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), $conditionType), - ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $elseType)); - $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; - } + $holder = new ConditionalExpressionHolder([ + $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::intersect($targetParameter->getType(), $parameterType->getTarget())), + ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $ifType), true); + $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; + + $holder = new ConditionalExpressionHolder([ + $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::remove($targetParameter->getType(), $parameterType->getTarget())), + ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $elseType), true); + $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; } } @@ -3227,9 +3218,22 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } foreach ($conditionalExpressions as $conditionalExpression) { foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { - if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) { + if (!array_key_exists($holderExprString, $specifiedExpressions)) { continue 2; } + $specifiedHolder = $specifiedExpressions[$holderExprString]; + if (!$specifiedHolder->getCertainty()->equals($conditionalTypeHolder->getCertainty())) { + continue 2; + } + if ($conditionalExpression->useSubtypeForConditionMatching()) { + if (!$conditionalTypeHolder->getType()->isSuperTypeOf($specifiedHolder->getType())->yes()) { + continue 2; + } + } else { + if (!$specifiedHolder->equals($conditionalTypeHolder)) { + continue 2; + } + } } $conditions[$conditionalExprString][] = $conditionalExpression; From 6f358cdbd0c03e3f4eb44f42f04648e6d2291077 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 11 Apr 2026 12:17:01 +0000 Subject: [PATCH 3/5] Remove useSubtypeForConditionMatching flag, use two-pass matching instead Instead of a flag on ConditionalExpressionHolder to control whether condition matching uses equals() or isSuperTypeOf(), use a two-pass approach in filterBySpecifiedTypes(): Pass 1: exact matching via equals() (existing behavior) Pass 2: isSuperTypeOf for condition types with finite types (fallback) Pass 2 only runs when pass 1 found no matches for a target expression. This handles conditional parameter types where the condition is a union (e.g. 'value1'|'value2') that can't match a narrowed type ('value1') via equals(), but should match via isSuperTypeOf. The two-pass approach avoids regressions from using isSuperTypeOf globally: when scope merging creates both a specific condition (e.g. "if $key=2, then $value is Yes") and a broader condition (e.g. "if $key=0|2, then $value is Maybe"), exact matching in pass 1 prevents the broader condition from degrading variable certainty through extremeIdentity. The bug-5051 test expectations are updated to reflect more precise type narrowing: when narrowing $data to a specific constant, PHPStan now correctly determines which branch was taken and narrows related variables accordingly (e.g. $update becomes 'false' instead of 'bool' when $data === 1 because that branch always sets $update = false). Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ConditionalExpressionHolder.php | 6 --- src/Analyser/MutatingScope.php | 46 ++++++++++++++------ tests/PHPStan/Analyser/nsrt/bug-5051.php | 14 +++--- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/Analyser/ConditionalExpressionHolder.php b/src/Analyser/ConditionalExpressionHolder.php index 7e2fc68a039..69071839660 100644 --- a/src/Analyser/ConditionalExpressionHolder.php +++ b/src/Analyser/ConditionalExpressionHolder.php @@ -17,7 +17,6 @@ final class ConditionalExpressionHolder public function __construct( private array $conditionExpressionTypeHolders, private ExpressionTypeHolder $typeHolder, - private bool $useSubtypeForConditionMatching = false, ) { if (count($conditionExpressionTypeHolders) === 0) { @@ -38,11 +37,6 @@ public function getTypeHolder(): ExpressionTypeHolder return $this->typeHolder; } - public function useSubtypeForConditionMatching(): bool - { - return $this->useSubtypeForConditionMatching; - } - public function getKey(): string { $parts = []; diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 9a8ac92527f..7174c80d1ff 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1760,12 +1760,12 @@ private function enterFunctionLike( $holder = new ConditionalExpressionHolder([ $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::intersect($targetParameter->getType(), $parameterType->getTarget())), - ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $ifType), true); + ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $ifType)); $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; $holder = new ConditionalExpressionHolder([ $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::remove($targetParameter->getType(), $parameterType->getTarget())), - ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $elseType), true); + ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $elseType)); $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; } } @@ -3216,28 +3216,46 @@ 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)) { - continue 2; - } - $specifiedHolder = $specifiedExpressions[$holderExprString]; - if (!$specifiedHolder->getCertainty()->equals($conditionalTypeHolder->getCertainty())) { + if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) { continue 2; } - if ($conditionalExpression->useSubtypeForConditionMatching()) { - if (!$conditionalTypeHolder->getType()->isSuperTypeOf($specifiedHolder->getType())->yes()) { + } + + $conditions[$conditionalExprString][] = $conditionalExpression; + $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); + } + + // Pass 2: for condition types with finite types, use isSuperTypeOf + // This handles cases like conditional parameter types where the condition + // is a union (e.g. 'value1'|'value2') that won't match a narrowed type + // (e.g. 'value1') 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; } - } else { - if (!$specifiedHolder->equals($conditionalTypeHolder)) { + $specifiedHolder = $specifiedExpressions[$holderExprString]; + if (!$specifiedHolder->getCertainty()->equals($conditionalTypeHolder->getCertainty())) { + continue 2; + } + if (count($conditionalTypeHolder->getType()->getFiniteTypes()) === 0) { + continue 2; + } + if (!$conditionalTypeHolder->getType()->isSuperTypeOf($specifiedHolder->getType())->yes()) { continue 2; } } - } - $conditions[$conditionalExprString][] = $conditionalExpression; - $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); + $conditions[$conditionalExprString][] = $conditionalExpression; + $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); + } } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-5051.php b/tests/PHPStan/Analyser/nsrt/bug-5051.php index 6c3e80dce11..94ffc4711c8 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); From 79d59d6b55d62b178dd72c0bd012d375c42dd3c3 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 11 Apr 2026 15:55:39 +0000 Subject: [PATCH 4/5] Explain why getFiniteTypes() guard is needed in Pass 2 condition matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address reviewer question about why isSuperTypeOf cannot be used for all types. Conditional expression holders come from multiple sources (param conditional types, scope merging, assignment handlers, TypeSpecifier boolean processing). Non-finite condition types from scope merging and assignment handlers are too broad for isSuperTypeOf matching — e.g. non-falsy-string incorrectly matches 'filter', or mixed~false matches false, causing conflicting conditional expressions to produce *NEVER*. The finite types check restricts Pass 2 to closed sets of concrete values where subtype matching is safe. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 7174c80d1ff..f85a44d05d1 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3235,6 +3235,23 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self // (e.g. 'value1') 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. + // + // The getFiniteTypes() guard is necessary because conditional expression + // holders are created from multiple sources: + // 1. Conditional parameter types (@param conditional types) — these have + // finite condition types like 'value1'|'value2' from TypeCombinator::intersect/remove + // 2. Scope merging (generateConditionalExpressions) — condition types can be + // any type like mixed~null, object, non-falsy-string + // 3. Assignment handlers — condition types like mixed~false from falsey checks + // 4. TypeSpecifier boolean processing — condition types from BooleanAnd/Or + // + // Using isSuperTypeOf without the finite types guard causes regressions because + // non-finite condition types from sources 2-4 are too broad: e.g. a condition + // type of non-falsy-string would incorrectly match a narrowed type 'filter', + // or a condition type of mixed~false would match false, causing unrelated + // conditional expressions to activate and produce conflicting types (*NEVER*). + // The finite types check restricts Pass 2 to closed sets of concrete values + // (constant strings, booleans, enum cases, etc.) where subtype matching is safe. if (!array_key_exists($conditionalExprString, $conditions)) { foreach ($conditionalExpressions as $conditionalExpression) { foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { From fa16e7859cfb63b3d1b7569aab3cfafbb9845058 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 11 Apr 2026 16:36:50 +0000 Subject: [PATCH 5/5] Use UnionType guard instead of getFiniteTypes() in Pass 2 condition matching Replace the getFiniteTypes() guard with an instanceof UnionType check in Pass 2 of filterBySpecifiedTypes(). This broadens Pass 2 to handle all union condition types (not just finite ones), which fixes cases where scope merging creates union conditions like int|string that should match a narrowed type like int via isSuperTypeOf. The UnionType guard still prevents regressions from non-union condition types (non-falsy-string from TypeSpecifier, mixed~false from assignment handlers) that are too broad for isSuperTypeOf matching. Added test case for scope merging with is_string()||is_int() narrowing. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 39 ++++++++++------------- tests/PHPStan/Analyser/nsrt/bug-10055.php | 12 +++++++ 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index f85a44d05d1..d82655ad7af 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3229,29 +3229,22 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); } - // Pass 2: for condition types with finite types, use isSuperTypeOf - // This handles cases like conditional parameter types where the condition - // is a union (e.g. 'value1'|'value2') that won't match a narrowed type - // (e.g. 'value1') 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. + // Pass 2: for union condition types, use isSuperTypeOf + // This handles cases where the condition type is a union + // (e.g. 'value1'|'value2' or int|string) that won't match a narrowed + // type (e.g. 'value1' or int) 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. // - // The getFiniteTypes() guard is necessary because conditional expression - // holders are created from multiple sources: - // 1. Conditional parameter types (@param conditional types) — these have - // finite condition types like 'value1'|'value2' from TypeCombinator::intersect/remove - // 2. Scope merging (generateConditionalExpressions) — condition types can be - // any type like mixed~null, object, non-falsy-string - // 3. Assignment handlers — condition types like mixed~false from falsey checks - // 4. TypeSpecifier boolean processing — condition types from BooleanAnd/Or - // - // Using isSuperTypeOf without the finite types guard causes regressions because - // non-finite condition types from sources 2-4 are too broad: e.g. a condition - // type of non-falsy-string would incorrectly match a narrowed type 'filter', - // or a condition type of mixed~false would match false, causing unrelated - // conditional expressions to activate and produce conflicting types (*NEVER*). - // The finite types check restricts Pass 2 to closed sets of concrete values - // (constant strings, booleans, enum cases, etc.) where subtype matching is safe. + // The UnionType guard is necessary because using isSuperTypeOf for all + // condition types causes regressions: non-union types like + // non-falsy-string (from TypeSpecifier boolean processing) or + // mixed~false (from assignment handlers) are too broad — e.g. + // non-falsy-string would incorrectly match 'filter', and mixed~false + // matching false causes conflicting conditional expressions to both + // activate, producing *NEVER* types. Union types are safe because they + // explicitly enumerate alternatives where subtype matching is correct. if (!array_key_exists($conditionalExprString, $conditions)) { foreach ($conditionalExpressions as $conditionalExpression) { foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { @@ -3262,7 +3255,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self if (!$specifiedHolder->getCertainty()->equals($conditionalTypeHolder->getCertainty())) { continue 2; } - if (count($conditionalTypeHolder->getType()->getFiniteTypes()) === 0) { + if (!$conditionalTypeHolder->getType() instanceof UnionType) { continue 2; } if (!$conditionalTypeHolder->getType()->isSuperTypeOf($specifiedHolder->getType())->yes()) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-10055.php b/tests/PHPStan/Analyser/nsrt/bug-10055.php index 8dcfe7ecf18..2464880ffcc 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10055.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10055.php @@ -18,3 +18,15 @@ function test(string $param1, int|bool $param2): void '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); + } +}