Skip to content

Commit 77a3244

Browse files
committed
Fix broken type narrowing with larger union types
1 parent be965d1 commit 77a3244

File tree

2 files changed

+80
-5
lines changed

2 files changed

+80
-5
lines changed

src/Analyser/MutatingScope.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3076,9 +3076,9 @@ private function setExpressionCertainty(Expr $expr, TrinaryLogic $certainty): se
30763076
}
30773077

30783078
/**
3079-
* Returns true when the type is a large union with non-trivial
3080-
* (IntersectionType) members — a sign of HasOffsetValueType
3081-
* combinatorial growth from array|object offset access patterns.
3079+
* Returns true when the type is a large union with intersection
3080+
* members that carry HasOffsetValueType — a sign of combinatorial
3081+
* growth from successive array|object offset access patterns.
30823082
* Operating on such types is expensive and should be skipped.
30833083
*/
30843084
private function isComplexUnionType(Type $type): bool
@@ -3091,8 +3091,13 @@ private function isComplexUnionType(Type $type): bool
30913091
return false;
30923092
}
30933093
foreach ($types as $member) {
3094-
if ($member instanceof IntersectionType) {
3095-
return true;
3094+
if (!$member instanceof IntersectionType) {
3095+
continue;
3096+
}
3097+
foreach ($member->getTypes() as $innerType) {
3098+
if ($innerType instanceof HasOffsetValueType) {
3099+
return true;
3100+
}
30963101
}
30973102
}
30983103
return false;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace Bug14484;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class A {}
8+
class B {}
9+
class C {}
10+
class D {}
11+
12+
class Bug
13+
{
14+
/**
15+
* @return int|bool|string|A|B|C|D|list<A>|null
16+
*/
17+
public function getValue(): mixed
18+
{
19+
return null;
20+
}
21+
22+
public function test(): void
23+
{
24+
$value = $this->getValue();
25+
if (!is_string($value)) {
26+
return;
27+
}
28+
assertType('string', $value);
29+
}
30+
31+
}
32+
33+
class Bug2
34+
{
35+
/**
36+
* @return int|bool|string|A|B|C|D|list<A>|null
37+
*/
38+
public function getValue(): mixed
39+
{
40+
return null;
41+
}
42+
43+
public function testInstanceof(): void
44+
{
45+
$value = $this->getValue();
46+
if (!($value instanceof A)) {
47+
return;
48+
}
49+
// Expected: narrowed to A
50+
// Actual in 2.1.49: entire union reported (narrowing lost)
51+
assertType(A::class, $value);
52+
}
53+
54+
public function testIfElseifInstanceof(): void
55+
{
56+
$value = $this->getValue();
57+
if ($value === null) {
58+
return;
59+
}
60+
if ($value instanceof A) {
61+
assertType(A::class, $value);
62+
} elseif ($value instanceof B) {
63+
assertType(B::class, $value);
64+
} elseif (is_array($value)) {
65+
assertType('list<Bug14484\\A>', $value);
66+
} elseif (is_string($value)) {
67+
assertType('string', $value);
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)