Skip to content

Commit 79d59d6

Browse files
phpstan-botclaude
andcommitted
Explain why getFiniteTypes() guard is needed in Pass 2 condition matching
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 <noreply@anthropic.com>
1 parent 6f358cd commit 79d59d6

File tree

1 file changed

+17
-0
lines changed

1 file changed

+17
-0
lines changed

src/Analyser/MutatingScope.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3235,6 +3235,23 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
32353235
// (e.g. 'value1') via equals(), but should match via isSuperTypeOf.
32363236
// Only attempted when pass 1 found no matches, to avoid conflicts with
32373237
// broader conditions that have lower certainty from scope merging.
3238+
//
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.
32383255
if (!array_key_exists($conditionalExprString, $conditions)) {
32393256
foreach ($conditionalExpressions as $conditionalExpression) {
32403257
foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) {

0 commit comments

Comments
 (0)