Skip to content

Commit 0e0de1c

Browse files
phpstan-botclaude
andcommitted
Collect impure points from implicit __toString() in echo handler and add purity tests
The echo statement handler in NodeScopeResolver was not collecting impure points from implicit __toString() calls or from sub-expression processing. Also adds purity tests for all implicit __toString() call sites: echo, print, string concatenation, concat assignment, and string interpolation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 281d039 commit 0e0de1c

3 files changed

Lines changed: 151 additions & 7 deletions

File tree

src/Analyser/NodeScopeResolver.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -863,21 +863,22 @@ public function processStmtNode(
863863
} elseif ($stmt instanceof Echo_) {
864864
$hasYield = false;
865865
$throwPoints = [];
866+
$impurePoints = [];
866867
$isAlwaysTerminating = false;
867868
foreach ($stmt->exprs as $echoExpr) {
868869
$result = $this->processExprNode($stmt, $echoExpr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
869870
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
871+
$impurePoints = array_merge($impurePoints, $result->getImpurePoints());
870872
$toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($echoExpr, $scope);
871873
$throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints());
874+
$impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints());
872875
$scope = $result->getScope();
873876
$hasYield = $hasYield || $result->hasYield();
874877
$isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
875878
}
876879

877880
$throwPoints = $overridingThrowPoints ?? $throwPoints;
878-
$impurePoints = [
879-
new ImpurePoint($scope, $stmt, 'echo', 'echo', true),
880-
];
881+
$impurePoints[] = new ImpurePoint($scope, $stmt, 'echo', 'echo', true);
881882
return new InternalStatementResult($scope, $hasYield, $isAlwaysTerminating, [], $throwPoints, $impurePoints);
882883
} elseif ($stmt instanceof Return_) {
883884
if ($stmt->expr !== null) {

tests/PHPStan/Rules/Pure/PureMethodRuleTest.php

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,21 +133,89 @@ public function testRule(): void
133133
'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doFoo().',
134134
296,
135135
],
136+
[
137+
'Impure echo in pure method PureMethod\TestMagicMethods::doEcho().',
138+
309,
139+
],
140+
[
141+
'Impure echo in pure method PureMethod\TestMagicMethods::doEcho().',
142+
310,
143+
],
144+
[
145+
'Possibly impure call to method PureMethod\MaybePureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doEcho().',
146+
311,
147+
],
148+
[
149+
'Impure echo in pure method PureMethod\TestMagicMethods::doEcho().',
150+
311,
151+
],
152+
[
153+
'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doEcho().',
154+
312,
155+
],
156+
[
157+
'Impure echo in pure method PureMethod\TestMagicMethods::doEcho().',
158+
312,
159+
],
160+
[
161+
'Impure print in pure method PureMethod\TestMagicMethods::doPrint().',
162+
324,
163+
],
164+
[
165+
'Possibly impure call to method PureMethod\MaybePureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doPrint().',
166+
325,
167+
],
168+
[
169+
'Impure print in pure method PureMethod\TestMagicMethods::doPrint().',
170+
325,
171+
],
172+
[
173+
'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doPrint().',
174+
326,
175+
],
176+
[
177+
'Impure print in pure method PureMethod\TestMagicMethods::doPrint().',
178+
326,
179+
],
180+
[
181+
'Possibly impure call to method PureMethod\MaybePureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doConcat().',
182+
339,
183+
],
184+
[
185+
'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doConcat().',
186+
340,
187+
],
188+
[
189+
'Possibly impure call to method PureMethod\MaybePureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doConcatAssign().',
190+
355,
191+
],
192+
[
193+
'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doConcatAssign().',
194+
357,
195+
],
196+
[
197+
'Possibly impure call to method PureMethod\MaybePureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doInterpolation().',
198+
370,
199+
],
200+
[
201+
'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doInterpolation().',
202+
371,
203+
],
136204
[
137205
'Possibly impure call to a callable in pure method PureMethod\MaybeCallableFromUnion::doFoo().',
138-
330,
206+
405,
139207
],
140208
[
141209
'Possibly impure call to a callable in pure method PureMethod\MaybeCallableFromUnion::doFoo().',
142-
330,
210+
405,
143211
],
144212
[
145213
'Impure static property access in pure method PureMethod\StaticMethodAccessingStaticProperty::getA().',
146-
388,
214+
463,
147215
],
148216
[
149217
'Impure property assignment in pure method PureMethod\StaticMethodAssigningStaticProperty::getA().',
150-
409,
218+
484,
151219
],
152220
]);
153221
}

tests/PHPStan/Rules/Pure/data/pure-method.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,81 @@ public function doFoo(
296296
(string) $impure;
297297
}
298298

299+
/**
300+
* @phpstan-pure
301+
*/
302+
public function doEcho(
303+
NoMagicMethods $no,
304+
PureMagicMethods $pure,
305+
MaybePureMagicMethods $maybe,
306+
ImpureMagicMethods $impure
307+
)
308+
{
309+
echo $no;
310+
echo $pure;
311+
echo $maybe;
312+
echo $impure;
313+
}
314+
315+
/**
316+
* @phpstan-pure
317+
*/
318+
public function doPrint(
319+
PureMagicMethods $pure,
320+
MaybePureMagicMethods $maybe,
321+
ImpureMagicMethods $impure
322+
)
323+
{
324+
print $pure;
325+
print $maybe;
326+
print $impure;
327+
}
328+
329+
/**
330+
* @phpstan-pure
331+
*/
332+
public function doConcat(
333+
PureMagicMethods $pure,
334+
MaybePureMagicMethods $maybe,
335+
ImpureMagicMethods $impure
336+
)
337+
{
338+
'hello' . $pure;
339+
'hello' . $maybe;
340+
'hello' . $impure;
341+
}
342+
343+
/**
344+
* @phpstan-pure
345+
*/
346+
public function doConcatAssign(
347+
PureMagicMethods $pure,
348+
MaybePureMagicMethods $maybe,
349+
ImpureMagicMethods $impure
350+
)
351+
{
352+
$x = 'hello';
353+
$x .= $pure;
354+
$x = 'hello';
355+
$x .= $maybe;
356+
$x = 'hello';
357+
$x .= $impure;
358+
}
359+
360+
/**
361+
* @phpstan-pure
362+
*/
363+
public function doInterpolation(
364+
PureMagicMethods $pure,
365+
MaybePureMagicMethods $maybe,
366+
ImpureMagicMethods $impure
367+
)
368+
{
369+
"hello $pure";
370+
"hello $maybe";
371+
"hello $impure";
372+
}
373+
299374
}
300375

301376
final class NoConstructor

0 commit comments

Comments
 (0)