From afce9d0370a311ef7b5598a166c22f80879d295e Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 16 May 2026 13:42:41 +0000 Subject: [PATCH 1/3] Invalidate expressions after dynamic method calls Dynamic method calls like `$this->{'compileSection'}()` or `$this->{$variable}()` could not be resolved to a method reflection, so the else branch (unknown method) did not invalidate the callee expression. Since we cannot determine what the method does, treat it like a method with certain side effects and fully invalidate. Closes https://github.com/phpstan/phpstan/issues/3831 Co-Authored-By: Claude Opus 4.6 --- .../ExprHandler/MethodCallHandler.php | 2 + tests/PHPStan/Analyser/nsrt/bug-3831.php | 43 +++++++++++++++ ...isonOperatorsConstantConditionRuleTest.php | 5 ++ .../Rules/Comparison/data/bug-3831.php | 52 +++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-3831.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-3831.php diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 28e0181c331..f2dc9fd9af2 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -176,6 +176,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } else { + $nodeScopeResolver->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage); + $scope = $scope->invalidateExpression($normalizedExpr->var, true); $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); } $hasYield = $hasYield || $argsResult->hasYield(); diff --git a/tests/PHPStan/Analyser/nsrt/bug-3831.php b/tests/PHPStan/Analyser/nsrt/bug-3831.php new file mode 100644 index 00000000000..144bdef7217 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3831.php @@ -0,0 +1,43 @@ + */ + public array $footer = []; + + public function test(): void + { + $this->counter = 0; + assertType('0', $this->counter); + + $this->{'increment'}(); + assertType('int', $this->counter); + } + + public function testDynamicVar(): void + { + $this->footer = []; + assertType('array{}', $this->footer); + + $method = 'compileSection'; + $this->{$method}(); + assertType('array', $this->footer); + } + + private function increment(): int + { + $this->counter++; + return 0; + } + + private function compileSection(): void + { + $this->footer[] = 'section-name'; + } +} diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index b87408f1499..8e7cd3c3c10 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -308,4 +308,9 @@ public function testBug11146(): void $this->analyse([__DIR__ . '/data/bug-11146.php'], []); } + public function testBug3831(): void + { + $this->analyse([__DIR__ . '/data/bug-3831.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3831.php b/tests/PHPStan/Rules/Comparison/data/bug-3831.php new file mode 100644 index 00000000000..2548965a405 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3831.php @@ -0,0 +1,52 @@ + */ + public $footer = []; + + public function render(): string + { + $content = ''; + $this->footer = []; + + $this->{'compileSection'}(); + + if (count($this->footer) > 0) { + $content = str_replace('some', 'thing', $content); + } + return $content; + } + + private function compileSection(): void + { + $this->footer[] = 'section-name'; + } +} + +class TemplateDynamicVar +{ + /** @var array */ + public $footer = []; + + public function render(): string + { + $content = ''; + $this->footer = []; + + $method = 'compileSection'; + $this->{$method}(); + + if (count($this->footer) > 0) { + $content = str_replace('some', 'thing', $content); + } + return $content; + } + + private function compileSection(): void + { + $this->footer[] = 'section-name'; + } +} From 671a2dc0157f3e41893e2a0f437705e029e0112b Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 16 May 2026 19:05:22 +0000 Subject: [PATCH 2/3] Load nsrt file in RuleTest to avoid duplicate fixture Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-3831.php | 49 +++++++++++++++++ ...isonOperatorsConstantConditionRuleTest.php | 2 +- .../Rules/Comparison/data/bug-3831.php | 52 ------------------- 3 files changed, 50 insertions(+), 53 deletions(-) delete mode 100644 tests/PHPStan/Rules/Comparison/data/bug-3831.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-3831.php b/tests/PHPStan/Analyser/nsrt/bug-3831.php index 144bdef7217..cb0fab1a447 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3831.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3831.php @@ -41,3 +41,52 @@ private function compileSection(): void $this->footer[] = 'section-name'; } } + +class Template +{ + /** @var array */ + public $footer = []; + + public function render(): string + { + $content = ''; + $this->footer = []; + + $this->{'compileSection'}(); + + if (count($this->footer) > 0) { + $content = str_replace('some', 'thing', $content); + } + return $content; + } + + private function compileSection(): void + { + $this->footer[] = 'section-name'; + } +} + +class TemplateDynamicVar +{ + /** @var array */ + public $footer = []; + + public function render(): string + { + $content = ''; + $this->footer = []; + + $method = 'compileSection'; + $this->{$method}(); + + if (count($this->footer) > 0) { + $content = str_replace('some', 'thing', $content); + } + return $content; + } + + private function compileSection(): void + { + $this->footer[] = 'section-name'; + } +} diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 8e7cd3c3c10..48dce91125c 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -310,7 +310,7 @@ public function testBug11146(): void public function testBug3831(): void { - $this->analyse([__DIR__ . '/data/bug-3831.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3831.php'], []); } } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3831.php b/tests/PHPStan/Rules/Comparison/data/bug-3831.php deleted file mode 100644 index 2548965a405..00000000000 --- a/tests/PHPStan/Rules/Comparison/data/bug-3831.php +++ /dev/null @@ -1,52 +0,0 @@ - */ - public $footer = []; - - public function render(): string - { - $content = ''; - $this->footer = []; - - $this->{'compileSection'}(); - - if (count($this->footer) > 0) { - $content = str_replace('some', 'thing', $content); - } - return $content; - } - - private function compileSection(): void - { - $this->footer[] = 'section-name'; - } -} - -class TemplateDynamicVar -{ - /** @var array */ - public $footer = []; - - public function render(): string - { - $content = ''; - $this->footer = []; - - $method = 'compileSection'; - $this->{$method}(); - - if (count($this->footer) > 0) { - $content = str_replace('some', 'thing', $content); - } - return $content; - } - - private function compileSection(): void - { - $this->footer[] = 'section-name'; - } -} From 89180e8213486bda0927599712bf82b0982ecdb1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 17 May 2026 10:13:01 +0200 Subject: [PATCH 3/3] more asserts --- tests/PHPStan/Analyser/nsrt/bug-3831.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-3831.php b/tests/PHPStan/Analyser/nsrt/bug-3831.php index cb0fab1a447..b0459f5b298 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3831.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3831.php @@ -52,7 +52,9 @@ public function render(): string $content = ''; $this->footer = []; + assertType('array{}', $this->footer); $this->{'compileSection'}(); + assertType('array', $this->footer); if (count($this->footer) > 0) { $content = str_replace('some', 'thing', $content); @@ -76,8 +78,10 @@ public function render(): string $content = ''; $this->footer = []; + assertType('array{}', $this->footer); $method = 'compileSection'; $this->{$method}(); + assertType('array', $this->footer); if (count($this->footer) > 0) { $content = str_replace('some', 'thing', $content);