From 3dfe74f70d60be7fc00879b1fd35640e45a8b980 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:41:16 +0000 Subject: [PATCH] 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 --- src/Analyser/MutatingScope.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-14545.php | 112 ++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14545.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 79d4be017bf..64e46074738 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2869,7 +2869,7 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require continue; } $firstExpr = $holders[array_key_first($holders)]->getTypeHolder()->getExpr(); - if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $firstExpr, $this->getNodeKey($firstExpr), false, $invalidatingClass)) { + if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $firstExpr, $this->getNodeKey($firstExpr), $requireMoreCharacters, $invalidatingClass)) { $invalidated = true; continue; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14545.php b/tests/PHPStan/Analyser/nsrt/bug-14545.php new file mode 100644 index 00000000000..e12b2e9df1b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14545.php @@ -0,0 +1,112 @@ + $class_name + * @return T + */ +function getObject1(string $class_name): object { + return new $class_name; +} + +function testStoredInstanceofWithGenericMethodCall(): void { + $obj = getObject1(ObjectClass::class); + $is_interface = $obj instanceof SomeInterface; + if($is_interface) { + assertType('Bug14545\ObjectClass&Bug14545\SomeInterface', $obj); + $obj->test(); + } + + if($is_interface) { + assertType('Bug14545\ObjectClass&Bug14545\SomeInterface', $obj); + $obj->test(); + } +} + +function testStoredInstanceofWithGenericFuncCall(): void { + $obj = getObject1(ObjectClass::class); + $is_interface = $obj instanceof SomeInterface; + if($is_interface) { + var_dump($obj); + } + + if($is_interface) { + assertType('Bug14545\ObjectClass&Bug14545\SomeInterface', $obj); + } +} + +function testStoredInstanceofWithConcreteClass(): void { + $obj = getObject1(OtherClass::class); + $is_interface = $obj instanceof SomeInterface; + if($is_interface) { + assertType('Bug14545\OtherClass&Bug14545\SomeInterface', $obj); + $obj->test(); + } + + if($is_interface) { + assertType('Bug14545\OtherClass&Bug14545\SomeInterface', $obj); + } +} + +function getObject2(): object { + return new \stdClass(); +} + +function testStoredInstanceofWithAbstractObject(): void { + $obj = getObject2(); + $is_interface = $obj instanceof SomeInterface; + if($is_interface) { + assertType('Bug14545\SomeInterface', $obj); + $obj->test(); + } + + if($is_interface) { + assertType('Bug14545\SomeInterface', $obj); + $obj->test(); + } +} + +function testThreeConsecutiveChecks(): void { + $obj = getObject1(ObjectClass::class); + $is_interface = $obj instanceof SomeInterface; + if($is_interface) { + $obj->test(); + } + if($is_interface) { + $obj->test(); + } + if($is_interface) { + assertType('Bug14545\ObjectClass&Bug14545\SomeInterface', $obj); + } +} + +/** + * @param array $data + */ +function testStoredIsArray(array $data): void { + $value = $data['key'] ?? null; + $isArray = is_array($value); + if ($isArray) { + assertType('array', $value); + var_dump($value); + } + if ($isArray) { + assertType('array', $value); + } +}