Skip to content

Commit 3f56074

Browse files
phpstan-botclaude
andcommitted
Use isSuperTypeOf for all types in conditional expression matching
Instead of restricting isSuperTypeOf to integer ranges, use a two-pass strategy: try exact matches first, then fall back to isSuperTypeOf when no exact match exists. The fallback only accepts a single match to avoid intersecting potentially conflicting results from overlapping conditions (e.g. mixed~null, mixed~false, mixed~0 all being supertypes of false). This generalizes the fix beyond IntegerRangeType while keeping existing behavior intact. Updated bug-5051 test expectations to reflect improved precision (PHPStan can now narrow $update to false when $data is known). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 13f95d5 commit 3f56074

File tree

2 files changed

+36
-27
lines changed

2 files changed

+36
-27
lines changed

src/Analyser/MutatingScope.php

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2154,24 +2154,7 @@ private function conditionalExpressionHolderMatches(ExpressionTypeHolder $specif
21542154
return false;
21552155
}
21562156

2157-
$conditionType = $condition->getType();
2158-
2159-
// Only use isSuperTypeOf for non-constant integer types (i.e. IntegerRangeType).
2160-
// This is needed for count()-in-variable patterns: when $count = count($a) creates
2161-
// a condition on int<1, max>, and $count is later narrowed to int<2, max> or 1,
2162-
// the subtype relationship correctly preserves the array narrowing.
2163-
//
2164-
// We cannot safely extend this to all types because filterBySpecifiedTypes()
2165-
// intersects results from ALL matching conditional expressions (line ~3251).
2166-
// For types like bool, isSuperTypeOf would match both a 'false' condition AND
2167-
// a 'bool' condition simultaneously, and intersecting their (potentially conflicting)
2168-
// results produces *NEVER*. Integer ranges don't have this problem because
2169-
// count() creates non-overlapping conditions (e.g. int<1, max> vs 0).
2170-
if (!$conditionType->isInteger()->yes() || $conditionType->isConstantScalarValue()->yes()) {
2171-
return false;
2172-
}
2173-
2174-
return $conditionType->isSuperTypeOf($specified->getType())->yes();
2157+
return $condition->getType()->isSuperTypeOf($specified->getType())->yes();
21752158
}
21762159

21772160
private function expressionTypeIsUnchangeable(ExpressionTypeHolder $typeHolder): bool
@@ -3246,15 +3229,41 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
32463229
if (array_key_exists($conditionalExprString, $conditions)) {
32473230
continue;
32483231
}
3232+
3233+
// Pass 1: exact matches
3234+
foreach ($conditionalExpressions as $conditionalExpression) {
3235+
$allExact = true;
3236+
foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) {
3237+
if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) {
3238+
$allExact = false;
3239+
break;
3240+
}
3241+
}
3242+
if ($allExact) {
3243+
$conditions[$conditionalExprString][] = $conditionalExpression;
3244+
$specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder();
3245+
}
3246+
}
3247+
3248+
if (array_key_exists($conditionalExprString, $conditions)) {
3249+
continue;
3250+
}
3251+
3252+
// Pass 2: isSuperTypeOf fallback when no exact match exists
3253+
$superTypeMatches = [];
32493254
foreach ($conditionalExpressions as $conditionalExpression) {
32503255
foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) {
32513256
if (!array_key_exists($holderExprString, $specifiedExpressions) || !$this->conditionalExpressionHolderMatches($specifiedExpressions[$holderExprString], $conditionalTypeHolder)) {
32523257
continue 2;
32533258
}
32543259
}
32553260

3256-
$conditions[$conditionalExprString][] = $conditionalExpression;
3257-
$specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder();
3261+
$superTypeMatches[] = $conditionalExpression;
3262+
}
3263+
3264+
if (count($superTypeMatches) === 1) {
3265+
$conditions[$conditionalExprString][] = $superTypeMatches[0];
3266+
$specifiedExpressions[$conditionalExprString] = $superTypeMatches[0]->getTypeHolder();
32583267
}
32593268
}
32603269
}

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,35 +60,35 @@ public function testWithBooleans($data): void
6060
assertType('bool', $update);
6161
} else {
6262
assertType('1|2', $data);
63-
assertType('bool', $update);
63+
assertType('false', $update);
6464
}
6565

6666
if ($data === 1) {
67-
assertType('bool', $update);
68-
assertType('bool', $foo);
67+
assertType('false', $update);
68+
assertType('false', $foo);
6969
} else {
7070
assertType('bool', $update);
7171
assertType('bool', $foo);
7272
}
7373

7474
if ($data === 2) {
75-
assertType('bool', $update);
76-
assertType('bool', $foo);
75+
assertType('false', $update);
76+
assertType('false', $foo);
7777
} else {
7878
assertType('bool', $update);
7979
assertType('bool', $foo);
8080
}
8181

8282
if ($data === 3) {
83-
assertType('bool', $update);
83+
assertType('false', $update);
8484
assertType('true', $foo);
8585
} else {
8686
assertType('bool', $update);
8787
assertType('bool', $foo);
8888
}
8989

9090
if ($data === 1 || $data === 2) {
91-
assertType('bool', $update);
91+
assertType('false', $update);
9292
assertType('false', $foo);
9393
} else {
9494
assertType('bool', $update);

0 commit comments

Comments
 (0)