Skip to content

Commit d2c6680

Browse files
phpstan-botclaude
andcommitted
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. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3b054a2 commit d2c6680

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

src/Analyser/ExprHandler/MethodCallHandler.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
179179
}
180180

181181
} else {
182+
$nodeScopeResolver->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage);
183+
$scope = $scope->invalidateExpression($normalizedExpr->var, true);
182184
$throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
183185
}
184186
$hasYield = $hasYield || $argsResult->hasYield();

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,41 @@ public function test(): void
118118
assertType('5', $this->x);
119119
}
120120
}
121+
122+
class DynamicMethodCall
123+
{
124+
public int $counter = 0;
125+
126+
/** @var array<string> */
127+
public array $footer = [];
128+
129+
public function test(): void
130+
{
131+
$this->counter = 0;
132+
assertType('0', $this->counter);
133+
134+
$this->{'increment'}();
135+
assertType('int', $this->counter);
136+
}
137+
138+
public function testDynamicVar(): void
139+
{
140+
$this->footer = [];
141+
assertType('array{}', $this->footer);
142+
143+
$method = 'compileSection';
144+
$this->{$method}();
145+
assertType('array<string>', $this->footer);
146+
}
147+
148+
private function increment(): int
149+
{
150+
$this->counter++;
151+
return 0;
152+
}
153+
154+
private function compileSection(): void
155+
{
156+
$this->footer[] = 'section-name';
157+
}
158+
}

tests/PHPStan/Rules/Comparison/data/bug-3831.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,52 @@ private function load(): array
6161
return [];
6262
}
6363
}
64+
65+
class Template
66+
{
67+
/** @var array<string> */
68+
public $footer = [];
69+
70+
public function render(): string
71+
{
72+
$content = '';
73+
$this->footer = [];
74+
75+
$this->{'compileSection'}();
76+
77+
if (count($this->footer) > 0) {
78+
$content = str_replace('some', 'thing', $content);
79+
}
80+
return $content;
81+
}
82+
83+
private function compileSection(): void
84+
{
85+
$this->footer[] = 'section-name';
86+
}
87+
}
88+
89+
class TemplateDynamicVar
90+
{
91+
/** @var array<string> */
92+
public $footer = [];
93+
94+
public function render(): string
95+
{
96+
$content = '';
97+
$this->footer = [];
98+
99+
$method = 'compileSection';
100+
$this->{$method}();
101+
102+
if (count($this->footer) > 0) {
103+
$content = str_replace('some', 'thing', $content);
104+
}
105+
return $content;
106+
}
107+
108+
private function compileSection(): void
109+
{
110+
$this->footer[] = 'section-name';
111+
}
112+
}

0 commit comments

Comments
 (0)