Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6885529

Browse files
ondrejmirtesclaude
andauthored
Merge conditional-expression holders for the same target
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 3c7977a commit 6885529

5 files changed

Lines changed: 127 additions & 7 deletions

File tree

src/Analyser/MutatingScope.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3354,7 +3354,18 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
33543354
public function addConditionalExpressions(string $exprString, array $conditionalExpressionHolders): self
33553355
{
33563356
$conditionalExpressions = $this->conditionalExpressions;
3357-
$conditionalExpressions[$exprString] = $conditionalExpressionHolders;
3357+
// Merge rather than overwrite: multiple independent holders can target the same
3358+
// expression (e.g. `$xIsA = $x instanceof A && $y instanceof A` stores a holder
3359+
// for `$x` keyed on `$xIsA`; later `$yIsA = $y instanceof A && $x instanceof A`
3360+
// stores another holder for the same target `$x` keyed on `$yIsA`). Replacing
3361+
// the existing entry here would throw away the earlier binding, breaking
3362+
// narrowing inside later `if ($xIsA) { … }` inside `if ($xIsA || $yIsA)`.
3363+
// Holder keys (`getKey()`) disambiguate identical entries so we still dedupe.
3364+
$existing = $conditionalExpressions[$exprString] ?? [];
3365+
foreach ($conditionalExpressionHolders as $holder) {
3366+
$existing[$holder->getKey()] = $holder;
3367+
}
3368+
$conditionalExpressions[$exprString] = $existing;
33583369
return $this->scopeFactory->create(
33593370
$this->context,
33603371
$this->isDeclareStrictTypes(),
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace Bug12677;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
public function getBool(): bool
10+
{
11+
return true;
12+
}
13+
}
14+
15+
function test1(?Foo $foo): void
16+
{
17+
$hasFoo = $foo !== null;
18+
19+
$bool = $foo?->getBool() ?? false;
20+
21+
if ($hasFoo) {
22+
assertType(Foo::class, $foo);
23+
}
24+
}
25+
26+
function test2(?Foo $foo): void
27+
{
28+
$hasFoo = $foo !== null;
29+
30+
$bool = $foo !== null ? $foo->getBool() : false;
31+
32+
if ($hasFoo) {
33+
assertType(Foo::class, $foo);
34+
}
35+
}
36+
37+
function test3(?Foo $foo): void
38+
{
39+
$hasFoo = $foo !== null;
40+
41+
$bool = $hasFoo ? $foo->getBool() : false;
42+
43+
if ($hasFoo) {
44+
assertType(Foo::class, $foo);
45+
}
46+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public function sayHello(array $array): int
1515
$hasBar = isset($array['bar']) && $array['bar'] > 1;
1616

1717
if ($hasFoo) {
18-
assertType('array{foo?: int, bar?: int}', $array);
18+
assertType('array{foo: int, bar?: int}', $array);
1919
assertType('int<2, max>', $array['foo']);
2020
return $array['foo'];
2121
}
@@ -44,7 +44,7 @@ public function sayHello2(array $array): int
4444
}
4545

4646
if ($hasBar) {
47-
assertType('array{foo?: int, bar?: int}', $array);
47+
assertType('array{foo?: int, bar: int}', $array);
4848
assertType('int<2, max>', $array['bar']);
4949
return $array['bar'];
5050
}

tests/PHPStan/Analyser/nsrt/multi-assign.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ function (bool $b): void {
5555
function (bool $b): void {
5656
$foo = $bar = $baz = $b;
5757
if ($bar) {
58-
assertType('bool', $b);
58+
assertType('true', $b);
5959
assertType('bool', $foo);
6060
assertType('true', $bar);
6161
assertType('bool', $baz);
6262
} else {
63-
assertType('bool', $b);
63+
assertType('false', $b);
6464
assertType('bool', $foo);
6565
assertType('false', $bar);
6666
assertType('bool', $baz);
@@ -70,12 +70,12 @@ function (bool $b): void {
7070
function (bool $b): void {
7171
$foo = $bar = $baz = $b;
7272
if ($baz) {
73-
assertType('bool', $b);
73+
assertType('true', $b);
7474
assertType('bool', $foo);
7575
assertType('bool', $bar);
7676
assertType('true', $baz);
7777
} else {
78-
assertType('bool', $b);
78+
assertType('false', $b);
7979
assertType('bool', $foo);
8080
assertType('bool', $bar);
8181
assertType('false', $baz);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace OrConditionTernaryNarrowing;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class A {}
8+
class B {}
9+
10+
/**
11+
* Mirrors the shape used in TypeCombinator::intersect() where
12+
* `$constArrayIsI` / `$constArrayIsJ` pair up with `$constArray` /
13+
* `$otherArray` picked via ternaries inside a nested for-loop over an
14+
* `$types` list that gets spliced during iteration.
15+
*
16+
* @param list<A|B> $types
17+
*/
18+
function loopWithTernary(array $types): void
19+
{
20+
$typesCount = count($types);
21+
for ($i = 0; $i < $typesCount; $i++) {
22+
for ($j = $i + 1; $j < $typesCount; $j++) {
23+
$iIsA = $types[$i] instanceof A && ($types[$j] instanceof A || $types[$j] instanceof B);
24+
$jIsA = $types[$j] instanceof A && ($types[$i] instanceof A || $types[$i] instanceof B);
25+
26+
if ($iIsA || $jIsA) {
27+
$a = $iIsA ? $types[$i] : $types[$j];
28+
29+
// `$a` is definitely A: when `$iIsA` holds, the ternary picks
30+
// `$types[$i]` which is A; when `$iIsA` is false, the outer
31+
// OR forces `$jIsA` so the ternary picks `$types[$j]` which
32+
// is A.
33+
assertType(A::class, $a);
34+
}
35+
}
36+
}
37+
}
38+
39+
/**
40+
* Same pattern but with plain variables — each `$xIsA`/`$yIsA` records two
41+
* narrowings (both `$x` and `$y`). The earlier holder (from the first
42+
* assignment) must survive the second assignment so that inside the
43+
* outer `if ($xIsA || $yIsA)` the nested `if ($xIsA)` can still fire the
44+
* conditional holders attached to `$xIsA`.
45+
*
46+
* @param A|B $x
47+
* @param A|B $y
48+
*/
49+
function twoStoredAndNarrowings($x, $y): void
50+
{
51+
$xBothA = $x instanceof A && $y instanceof A;
52+
$yBothA = $y instanceof A && $x instanceof A;
53+
54+
if ($xBothA || $yBothA) {
55+
if ($xBothA) {
56+
assertType(A::class, $x);
57+
assertType(A::class, $y);
58+
} else {
59+
assertType(A::class, $x);
60+
assertType(A::class, $y);
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)