Skip to content

Commit 4e20ef7

Browse files
phpstan-botclaude
andcommitted
Extract side-effect checks into ExprSideEffectsHelper
Move expression side-effect detection logic from TypeSpecifier into a dedicated ExprSideEffectsHelper class with public rememberFuncCall, rememberMethodCall, rememberStaticCall, and subExpressionsHaveSideEffects methods that encapsulate all purity and side-effect conditions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9fce389 commit 4e20ef7

3 files changed

Lines changed: 289 additions & 242 deletions

File tree

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\ArrayDimFetch;
8+
use PhpParser\Node\Expr\FuncCall;
9+
use PhpParser\Node\Expr\MethodCall;
10+
use PhpParser\Node\Expr\PropertyFetch;
11+
use PhpParser\Node\Expr\StaticCall;
12+
use PhpParser\Node\Expr\StaticPropertyFetch;
13+
use PhpParser\Node\Name;
14+
use PHPStan\DependencyInjection\AutowiredParameter;
15+
use PHPStan\DependencyInjection\AutowiredService;
16+
use PHPStan\Reflection\ReflectionProvider;
17+
18+
#[AutowiredService]
19+
final class ExprSideEffectsHelper
20+
{
21+
22+
public function __construct(
23+
private ReflectionProvider $reflectionProvider,
24+
#[AutowiredParameter]
25+
private bool $rememberPossiblyImpureFunctionValues,
26+
)
27+
{
28+
}
29+
30+
public function rememberFuncCall(FuncCall $expr, Scope $scope): bool
31+
{
32+
if ($expr->name instanceof Name) {
33+
if (!$this->reflectionProvider->hasFunction($expr->name, $scope)) {
34+
return false;
35+
}
36+
37+
$functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
38+
$hasSideEffects = $functionReflection->hasSideEffects();
39+
if ($hasSideEffects->yes()) {
40+
return false;
41+
}
42+
43+
if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) {
44+
return false;
45+
}
46+
} else {
47+
$nameType = $scope->getType($expr->name);
48+
if ($nameType->isCallable()->yes()) {
49+
$isPure = null;
50+
foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) {
51+
$variantIsPure = $variant->isPure();
52+
$isPure = $isPure === null ? $variantIsPure : $isPure->and($variantIsPure);
53+
}
54+
55+
if ($isPure !== null) {
56+
if ($isPure->no()) {
57+
return false;
58+
}
59+
60+
if (!$this->rememberPossiblyImpureFunctionValues && !$isPure->yes()) {
61+
return false;
62+
}
63+
}
64+
}
65+
}
66+
67+
return !$this->callLikeArgsHaveSideEffects($expr, $scope);
68+
}
69+
70+
public function rememberMethodCall(MethodCall $expr, Scope $scope): bool
71+
{
72+
if (!$expr->name instanceof Node\Identifier) {
73+
return false;
74+
}
75+
76+
$methodName = $expr->name->toString();
77+
$calledOnType = $scope->getType($expr->var);
78+
$methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
79+
80+
if (
81+
$methodReflection === null
82+
|| $methodReflection->hasSideEffects()->yes()
83+
|| (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
84+
|| $this->expressionHasSideEffects($expr->var, $scope)
85+
|| $this->callLikeArgsHaveSideEffects($expr, $scope)
86+
) {
87+
return false;
88+
}
89+
90+
return true;
91+
}
92+
93+
public function rememberStaticCall(StaticCall $expr, Scope $scope): bool
94+
{
95+
if (!$expr->name instanceof Node\Identifier) {
96+
return false;
97+
}
98+
99+
$methodName = $expr->name->toString();
100+
if ($expr->class instanceof Name) {
101+
$calledOnType = $scope->resolveTypeByName($expr->class);
102+
} else {
103+
$calledOnType = $scope->getType($expr->class);
104+
}
105+
106+
$methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
107+
108+
if (
109+
$methodReflection === null
110+
|| $methodReflection->hasSideEffects()->yes()
111+
|| (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
112+
|| ($expr->class instanceof Expr && $this->expressionHasSideEffects($expr->class, $scope))
113+
|| $this->callLikeArgsHaveSideEffects($expr, $scope)
114+
) {
115+
return false;
116+
}
117+
118+
return true;
119+
}
120+
121+
public function subExpressionsHaveSideEffects(Expr $expr, Scope $scope): bool
122+
{
123+
if (
124+
$expr instanceof MethodCall
125+
|| $expr instanceof Expr\NullsafeMethodCall
126+
|| $expr instanceof PropertyFetch
127+
|| $expr instanceof Expr\NullsafePropertyFetch
128+
|| $expr instanceof ArrayDimFetch
129+
) {
130+
if ($this->expressionHasSideEffects($expr->var, $scope)) {
131+
return true;
132+
}
133+
} elseif (
134+
$expr instanceof StaticCall
135+
|| $expr instanceof StaticPropertyFetch
136+
) {
137+
if ($expr->class instanceof Expr && $this->expressionHasSideEffects($expr->class, $scope)) {
138+
return true;
139+
}
140+
}
141+
142+
if ($expr instanceof Expr\CallLike && $this->callLikeArgsHaveSideEffects($expr, $scope)) {
143+
return true;
144+
}
145+
146+
return false;
147+
}
148+
149+
private function callLikeArgsHaveSideEffects(Expr\CallLike $expr, Scope $scope): bool
150+
{
151+
if ($expr->isFirstClassCallable()) {
152+
return false;
153+
}
154+
155+
foreach ($expr->getArgs() as $arg) {
156+
if ($this->expressionHasSideEffects($arg->value, $scope)) {
157+
return true;
158+
}
159+
}
160+
161+
return false;
162+
}
163+
164+
private function expressionHasSideEffects(Expr $expr, Scope $scope): bool
165+
{
166+
if ($expr instanceof Expr\New_) {
167+
return true;
168+
}
169+
170+
if ($expr instanceof FuncCall) {
171+
if ($expr->isFirstClassCallable()) {
172+
return false;
173+
}
174+
if ($expr->name instanceof Name) {
175+
if (!$this->reflectionProvider->hasFunction($expr->name, $scope)) {
176+
return true;
177+
}
178+
$functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
179+
$hasSideEffects = $functionReflection->hasSideEffects();
180+
if ($hasSideEffects->yes()) {
181+
return true;
182+
}
183+
if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) {
184+
return true;
185+
}
186+
} else {
187+
return true;
188+
}
189+
foreach ($expr->getArgs() as $arg) {
190+
if ($this->expressionHasSideEffects($arg->value, $scope)) {
191+
return true;
192+
}
193+
}
194+
return false;
195+
}
196+
197+
if ($expr instanceof MethodCall || $expr instanceof Expr\NullsafeMethodCall) {
198+
if ($expr->isFirstClassCallable()) {
199+
return $this->expressionHasSideEffects($expr->var, $scope);
200+
}
201+
if ($expr->name instanceof Node\Identifier) {
202+
$calledOnType = $scope->getType($expr->var);
203+
$methodReflection = $scope->getMethodReflection($calledOnType, $expr->name->toString());
204+
if (
205+
$methodReflection === null
206+
|| $methodReflection->hasSideEffects()->yes()
207+
|| (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
208+
) {
209+
return true;
210+
}
211+
} else {
212+
return true;
213+
}
214+
foreach ($expr->getArgs() as $arg) {
215+
if ($this->expressionHasSideEffects($arg->value, $scope)) {
216+
return true;
217+
}
218+
}
219+
return $this->expressionHasSideEffects($expr->var, $scope);
220+
}
221+
222+
if ($expr instanceof StaticCall) {
223+
if ($expr->isFirstClassCallable()) {
224+
if ($expr->class instanceof Expr) {
225+
return $this->expressionHasSideEffects($expr->class, $scope);
226+
}
227+
return false;
228+
}
229+
if ($expr->name instanceof Node\Identifier) {
230+
if ($expr->class instanceof Name) {
231+
$calledOnType = $scope->resolveTypeByName($expr->class);
232+
} else {
233+
$calledOnType = $scope->getType($expr->class);
234+
}
235+
$methodReflection = $scope->getMethodReflection($calledOnType, $expr->name->toString());
236+
if (
237+
$methodReflection === null
238+
|| $methodReflection->hasSideEffects()->yes()
239+
|| (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
240+
) {
241+
return true;
242+
}
243+
} else {
244+
return true;
245+
}
246+
foreach ($expr->getArgs() as $arg) {
247+
if ($this->expressionHasSideEffects($arg->value, $scope)) {
248+
return true;
249+
}
250+
}
251+
if ($expr->class instanceof Expr) {
252+
return $this->expressionHasSideEffects($expr->class, $scope);
253+
}
254+
return false;
255+
}
256+
257+
if ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) {
258+
return $this->expressionHasSideEffects($expr->var, $scope);
259+
}
260+
261+
if ($expr instanceof ArrayDimFetch) {
262+
return $this->expressionHasSideEffects($expr->var, $scope);
263+
}
264+
265+
if ($expr instanceof StaticPropertyFetch) {
266+
if ($expr->class instanceof Expr) {
267+
return $this->expressionHasSideEffects($expr->class, $scope);
268+
}
269+
return false;
270+
}
271+
272+
return false;
273+
}
274+
275+
}

0 commit comments

Comments
 (0)