Skip to content

Commit fa16e78

Browse files
phpstan-botclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 79d59d6 commit fa16e78

2 files changed

Lines changed: 28 additions & 23 deletions

File tree

src/Analyser/MutatingScope.php

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3229,29 +3229,22 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
32293229
$specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder();
32303230
}
32313231

3232-
// Pass 2: for condition types with finite types, use isSuperTypeOf
3233-
// This handles cases like conditional parameter types where the condition
3234-
// is a union (e.g. 'value1'|'value2') that won't match a narrowed type
3235-
// (e.g. 'value1') via equals(), but should match via isSuperTypeOf.
3236-
// Only attempted when pass 1 found no matches, to avoid conflicts with
3237-
// broader conditions that have lower certainty from scope merging.
3232+
// Pass 2: for union condition types, use isSuperTypeOf
3233+
// This handles cases where the condition type is a union
3234+
// (e.g. 'value1'|'value2' or int|string) that won't match a narrowed
3235+
// type (e.g. 'value1' or int) via equals(), but should match via
3236+
// isSuperTypeOf. Only attempted when pass 1 found no matches, to avoid
3237+
// conflicts with broader conditions that have lower certainty from
3238+
// scope merging.
32383239
//
3239-
// The getFiniteTypes() guard is necessary because conditional expression
3240-
// holders are created from multiple sources:
3241-
// 1. Conditional parameter types (@param conditional types) — these have
3242-
// finite condition types like 'value1'|'value2' from TypeCombinator::intersect/remove
3243-
// 2. Scope merging (generateConditionalExpressions) — condition types can be
3244-
// any type like mixed~null, object, non-falsy-string
3245-
// 3. Assignment handlers — condition types like mixed~false from falsey checks
3246-
// 4. TypeSpecifier boolean processing — condition types from BooleanAnd/Or
3247-
//
3248-
// Using isSuperTypeOf without the finite types guard causes regressions because
3249-
// non-finite condition types from sources 2-4 are too broad: e.g. a condition
3250-
// type of non-falsy-string would incorrectly match a narrowed type 'filter',
3251-
// or a condition type of mixed~false would match false, causing unrelated
3252-
// conditional expressions to activate and produce conflicting types (*NEVER*).
3253-
// The finite types check restricts Pass 2 to closed sets of concrete values
3254-
// (constant strings, booleans, enum cases, etc.) where subtype matching is safe.
3240+
// The UnionType guard is necessary because using isSuperTypeOf for all
3241+
// condition types causes regressions: non-union types like
3242+
// non-falsy-string (from TypeSpecifier boolean processing) or
3243+
// mixed~false (from assignment handlers) are too broad — e.g.
3244+
// non-falsy-string would incorrectly match 'filter', and mixed~false
3245+
// matching false causes conflicting conditional expressions to both
3246+
// activate, producing *NEVER* types. Union types are safe because they
3247+
// explicitly enumerate alternatives where subtype matching is correct.
32553248
if (!array_key_exists($conditionalExprString, $conditions)) {
32563249
foreach ($conditionalExpressions as $conditionalExpression) {
32573250
foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) {
@@ -3262,7 +3255,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
32623255
if (!$specifiedHolder->getCertainty()->equals($conditionalTypeHolder->getCertainty())) {
32633256
continue 2;
32643257
}
3265-
if (count($conditionalTypeHolder->getType()->getFiniteTypes()) === 0) {
3258+
if (!$conditionalTypeHolder->getType() instanceof UnionType) {
32663259
continue 2;
32673260
}
32683261
if (!$conditionalTypeHolder->getType()->isSuperTypeOf($specifiedHolder->getType())->yes()) {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,15 @@ function test(string $param1, int|bool $param2): void
1818
'value3' => assertType('bool', $param2),
1919
};
2020
}
21+
22+
function testScopeMerging(mixed $foo): void
23+
{
24+
$a = 0;
25+
if (\is_string($foo) || \is_int($foo)) {
26+
$a = 1;
27+
}
28+
29+
if (\is_int($foo)) {
30+
assertType('1', $a);
31+
}
32+
}

0 commit comments

Comments
 (0)