From 16049a0da30e0c09230415d8867dd451999d81ba Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 30 Oct 2025 11:15:23 +0100 Subject: [PATCH] Don't forget property-fetch expressions on `$this` after static method call --- src/Analyser/MutatingScope.php | 14 +++++- src/Analyser/NodeScopeResolver.php | 7 ++- src/Node/Expr/AfterStaticMethodCall.php | 27 +++++++++++ src/Node/Printer/Printer.php | 6 +++ tests/PHPStan/Analyser/nsrt/bug-13735.php | 59 +++++++++++++++++++++++ 5 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/Node/Expr/AfterStaticMethodCall.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13735.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 9eaff99386..0f50c5ddf4 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -32,6 +32,7 @@ use PhpParser\Node\Stmt\Function_; use PhpParser\NodeFinder; use PHPStan\Node\ExecutionEndNode; +use PHPStan\Node\Expr\AfterStaticMethodCall; use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\GetIterableKeyTypeExpr; @@ -4561,9 +4562,9 @@ private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr $nodeFinder = new NodeFinder(); $expressionToInvalidateClass = get_class($exprToInvalidate); - $found = $nodeFinder->findFirst([$expr], function (Node $node) use ($expressionToInvalidateClass, $exprStringToInvalidate): bool { + $found = $nodeFinder->findFirst([$expr], function (Node $node) use ($expressionToInvalidateClass, $exprToInvalidate, $exprStringToInvalidate): bool { if ( - $exprStringToInvalidate === '$this' + ($exprStringToInvalidate === '$this' || $exprToInvalidate instanceof AfterStaticMethodCall) && $node instanceof Name && ( in_array($node->toLowerString(), ['self', 'static', 'parent'], true) @@ -4573,6 +4574,15 @@ private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr return true; } + if ( + $exprToInvalidate instanceof AfterStaticMethodCall + && $node instanceof MethodCall + && $node->var instanceof Variable + && $node->var->name === 'this' + ) { + return true; + } + if (!$node instanceof $expressionToInvalidateClass) { return false; } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index c91365fcf1..4d0ff7bdff 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -84,6 +84,7 @@ use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Node\DoWhileLoopConditionNode; use PHPStan\Node\ExecutionEndNode; +use PHPStan\Node\Expr\AfterStaticMethodCall; use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\ForeachValueByRefExpr; @@ -3117,7 +3118,11 @@ static function (): void { && $scope->isInClass() && $scope->getClassReflection()->is($methodReflection->getDeclaringClass()->getName()) ) { - $scope = $scope->invalidateExpression(new Variable('this'), true); + if ($methodReflection->isStatic()) { + $scope = $scope->invalidateExpression(new AfterStaticMethodCall(), true); + } else { + $scope = $scope->invalidateExpression(new Variable('this'), true); + } } if ( diff --git a/src/Node/Expr/AfterStaticMethodCall.php b/src/Node/Expr/AfterStaticMethodCall.php new file mode 100644 index 0000000000..369a4f0ea1 --- /dev/null +++ b/src/Node/Expr/AfterStaticMethodCall.php @@ -0,0 +1,27 @@ +getExprType()->describe(VerbosityLevel::precise())); diff --git a/tests/PHPStan/Analyser/nsrt/bug-13735.php b/tests/PHPStan/Analyser/nsrt/bug-13735.php new file mode 100644 index 0000000000..bfd18626cc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13735.php @@ -0,0 +1,59 @@ +foo = new Foo(); + assertType('Bug13735\Foo', $this->foo); + self::assertTrue(true); + assertType('Bug13735\Foo', $this->foo); + + assertType('bool', $this->foo->aBool); + $this->foo->aBool = true; + assertType('true', $this->foo->aBool); + self::assertTrue(true); + assertType('true', $this->foo->aBool); + } + + public function testCallFoo(): void + { + if ($this->getFoo() === null) { + return; + } + + // the getFoo() method could reference a static property in its body, + // so self::assertTrue() still needs to invalidate $this->getFoo(). + assertType('Bug13735\Foo', $this->getFoo()); + self::assertTrue(true); + assertType('Bug13735\Foo|null', $this->getFoo()); + } + + public function testStaticFoo(): void + { + self::$staticFoo = new Foo(); + assertType('Bug13735\Foo', self::$staticFoo); + self::assertTrue(true); + assertType('Bug13735\Foo|null', self::$staticFoo); + } + + public static function assertTrue(mixed $condition, string $message = ''): void + { + } + + public function getFoo(): ?Foo { + return rand(0 ,1) ? null : new Foo(); + } +} + +class Foo { + public bool $aBool = false; +}