Skip to content

Commit dc2650e

Browse files
phpstan-botondrejmirtes
authored andcommitted
Preserve conditional expressions in invalidateExpression when requireMoreCharacters is true
- In `MutatingScope::invalidateExpression()`, the target expression check for conditional expressions was hardcoded to `requireMoreCharacters=false`, causing conditional expressions to be removed even when the expression type itself was preserved (e.g. after a method call on the variable) - Pass the caller's `$requireMoreCharacters` flag to the `shouldInvalidateExpression` check for conditional expression targets, consistent with the expression type check - This fixes stored `instanceof` results losing their type narrowing after the first use when the if-block body contains impure calls (method calls, var_dump, etc.) - The bug only manifested for concrete class types (e.g. `ObjectClass`) where `createConditionalExpressions` could not reconstruct the conditional from type differences, unlike abstract `object` types where it could
1 parent 2f825fc commit dc2650e

2 files changed

Lines changed: 113 additions & 1 deletion

File tree

src/Analyser/MutatingScope.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2869,7 +2869,7 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require
28692869
continue;
28702870
}
28712871
$firstExpr = $holders[array_key_first($holders)]->getTypeHolder()->getExpr();
2872-
if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $firstExpr, $this->getNodeKey($firstExpr), false, $invalidatingClass)) {
2872+
if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $firstExpr, $this->getNodeKey($firstExpr), $requireMoreCharacters, $invalidatingClass)) {
28732873
$invalidated = true;
28742874
continue;
28752875
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14545;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
interface SomeInterface {
10+
public function test(): void;
11+
}
12+
13+
class ObjectClass {
14+
}
15+
16+
class OtherClass {
17+
}
18+
19+
/**
20+
* @template T of object
21+
* @param class-string<T> $class_name
22+
* @return T
23+
*/
24+
function getObject1(string $class_name): object {
25+
return new $class_name;
26+
}
27+
28+
function testStoredInstanceofWithGenericMethodCall(): void {
29+
$obj = getObject1(ObjectClass::class);
30+
$is_interface = $obj instanceof SomeInterface;
31+
if($is_interface) {
32+
assertType('Bug14545\ObjectClass&Bug14545\SomeInterface', $obj);
33+
$obj->test();
34+
}
35+
36+
if($is_interface) {
37+
assertType('Bug14545\ObjectClass&Bug14545\SomeInterface', $obj);
38+
$obj->test();
39+
}
40+
}
41+
42+
function testStoredInstanceofWithGenericFuncCall(): void {
43+
$obj = getObject1(ObjectClass::class);
44+
$is_interface = $obj instanceof SomeInterface;
45+
if($is_interface) {
46+
var_dump($obj);
47+
}
48+
49+
if($is_interface) {
50+
assertType('Bug14545\ObjectClass&Bug14545\SomeInterface', $obj);
51+
}
52+
}
53+
54+
function testStoredInstanceofWithConcreteClass(): void {
55+
$obj = getObject1(OtherClass::class);
56+
$is_interface = $obj instanceof SomeInterface;
57+
if($is_interface) {
58+
assertType('Bug14545\OtherClass&Bug14545\SomeInterface', $obj);
59+
$obj->test();
60+
}
61+
62+
if($is_interface) {
63+
assertType('Bug14545\OtherClass&Bug14545\SomeInterface', $obj);
64+
}
65+
}
66+
67+
function getObject2(): object {
68+
return new \stdClass();
69+
}
70+
71+
function testStoredInstanceofWithAbstractObject(): void {
72+
$obj = getObject2();
73+
$is_interface = $obj instanceof SomeInterface;
74+
if($is_interface) {
75+
assertType('Bug14545\SomeInterface', $obj);
76+
$obj->test();
77+
}
78+
79+
if($is_interface) {
80+
assertType('Bug14545\SomeInterface', $obj);
81+
$obj->test();
82+
}
83+
}
84+
85+
function testThreeConsecutiveChecks(): void {
86+
$obj = getObject1(ObjectClass::class);
87+
$is_interface = $obj instanceof SomeInterface;
88+
if($is_interface) {
89+
$obj->test();
90+
}
91+
if($is_interface) {
92+
$obj->test();
93+
}
94+
if($is_interface) {
95+
assertType('Bug14545\ObjectClass&Bug14545\SomeInterface', $obj);
96+
}
97+
}
98+
99+
/**
100+
* @param array<mixed, mixed> $data
101+
*/
102+
function testStoredIsArray(array $data): void {
103+
$value = $data['key'] ?? null;
104+
$isArray = is_array($value);
105+
if ($isArray) {
106+
assertType('array<mixed, mixed>', $value);
107+
var_dump($value);
108+
}
109+
if ($isArray) {
110+
assertType('array<mixed, mixed>', $value);
111+
}
112+
}

0 commit comments

Comments
 (0)