Skip to content

Commit f9f63e1

Browse files
committed
Do not let conditional expression undo explicit type narrowing
When filterBySpecifiedTypes processes a condition like `$this->b !== null`, it narrows the expression and then resolves conditional expression holders. A CE chain could derive `$this->a = null` (from mutual exclusion) and then use that to trigger a "remove $this->b from scope" CE, undoing the explicit narrowing. This fix skips certainty-No CEs for expressions that were already narrowed by the original type specifications. Closes phpstan/phpstan#14645
1 parent 03763ec commit f9f63e1

2 files changed

Lines changed: 118 additions & 0 deletions

File tree

src/Analyser/MutatingScope.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3298,6 +3298,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
32983298
}
32993299

33003300
$conditions = [];
3301+
$originallySpecifiedExprStrings = $specifiedExpressions;
33013302
$prevSpecifiedCount = -1;
33023303
while (count($specifiedExpressions) !== $prevSpecifiedCount) {
33033304
$prevSpecifiedCount = count($specifiedExpressions);
@@ -3308,6 +3309,12 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
33083309

33093310
// Pass 1: Prefer exact matches
33103311
foreach ($conditionalExpressions as $conditionalExpression) {
3312+
if (
3313+
$conditionalExpression->getTypeHolder()->getCertainty()->no()
3314+
&& array_key_exists($conditionalExprString, $originallySpecifiedExprStrings)
3315+
) {
3316+
continue;
3317+
}
33113318
foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) {
33123319
if (
33133320
!array_key_exists($holderExprString, $specifiedExpressions)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
namespace Bug14645;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
final class C
8+
{
9+
public function __construct(private ?self $a, private ?self $b)
10+
{
11+
}
12+
13+
public function check(): bool
14+
{
15+
if ($this->a !== null && $this->b !== null) {
16+
throw new \LogicException();
17+
}
18+
19+
if ($this->a !== null) {
20+
$this->a->check();
21+
}
22+
23+
if ($this->b !== null) {
24+
assertType('Bug14645\C', $this->b);
25+
$this->b->check();
26+
}
27+
28+
return true;
29+
}
30+
}
31+
32+
final class VariableAnalog
33+
{
34+
public function test(?\stdClass $a, ?\stdClass $b): void
35+
{
36+
if ($a !== null && $b !== null) {
37+
throw new \LogicException();
38+
}
39+
40+
if ($a !== null) {
41+
echo $a->foo;
42+
}
43+
44+
if ($b !== null) {
45+
assertType('stdClass', $b);
46+
echo $b->bar;
47+
}
48+
}
49+
}
50+
51+
final class ReversedOrder
52+
{
53+
public function __construct(private ?self $a, private ?self $b)
54+
{
55+
}
56+
57+
public function check(): bool
58+
{
59+
if ($this->a !== null && $this->b !== null) {
60+
throw new \LogicException();
61+
}
62+
63+
if ($this->b !== null) {
64+
$this->b->check();
65+
}
66+
67+
if ($this->a !== null) {
68+
assertType('Bug14645\ReversedOrder', $this->a);
69+
$this->a->check();
70+
}
71+
72+
return true;
73+
}
74+
}
75+
76+
final class ThreeProperties
77+
{
78+
public function __construct(
79+
private ?self $a,
80+
private ?self $b,
81+
private ?self $c,
82+
) {
83+
}
84+
85+
public function check(): bool
86+
{
87+
if ($this->a !== null && $this->b !== null) {
88+
throw new \LogicException();
89+
}
90+
91+
if ($this->a !== null && $this->c !== null) {
92+
throw new \LogicException();
93+
}
94+
95+
if ($this->a !== null) {
96+
$this->a->check();
97+
}
98+
99+
if ($this->b !== null) {
100+
assertType('Bug14645\ThreeProperties', $this->b);
101+
$this->b->check();
102+
}
103+
104+
if ($this->c !== null) {
105+
assertType('Bug14645\ThreeProperties', $this->c);
106+
$this->c->check();
107+
}
108+
109+
return true;
110+
}
111+
}

0 commit comments

Comments
 (0)