Skip to content

Commit e30122d

Browse files
phpstan-botclaude
andcommitted
Refactor call_user_func by-ref type resolution to reuse NodeScopeResolver logic
Extract resolveByRefParameterType() public method from NodeScopeResolver to centralize by-reference parameter type resolution (extension types, @param-out, builtin vs user-defined distinction). Use it in both processArgs and FuncCallHandler's call_user_func_array/call_user_func handling. Also resolves the inner callee reflection (function or method) from the callback type, and calls lookForUnsetAllowedUndefinedExpressions after virtual assignment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2d7ce79 commit e30122d

2 files changed

Lines changed: 80 additions & 25 deletions

File tree

src/Analyser/ExprHandler/FuncCallHandler.php

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@
3232
use PHPStan\Node\Expr\PossiblyImpureCallExpr;
3333
use PHPStan\Node\Expr\TypeExpr;
3434
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
35-
use PHPStan\Reflection\ExtendedParameterReflection;
3635
use PHPStan\Reflection\Callables\SimpleImpurePoint;
3736
use PHPStan\Reflection\Callables\SimpleThrowPoint;
3837
use PHPStan\Reflection\FunctionReflection;
38+
use PHPStan\Reflection\MethodReflection;
3939
use PHPStan\Reflection\ParametersAcceptor;
4040
use PHPStan\Reflection\ParametersAcceptorSelector;
4141
use PHPStan\Reflection\ReflectionProvider;
@@ -222,6 +222,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
222222
$innerParameters = $innerParametersAcceptor->getParameters();
223223
$innerArgs = $innerFuncCall->getArgs();
224224

225+
$innerCalleeReflection = $this->resolveCallUserFuncCalleeReflection($innerFuncCall, $scope);
226+
225227
foreach ($innerArgs as $i => $innerArg) {
226228
$innerParameter = null;
227229
if (isset($innerParameters[$i])) {
@@ -239,9 +241,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
239241
continue;
240242
}
241243

242-
$byRefType = $innerParameter instanceof ExtendedParameterReflection && $innerParameter->getOutType() !== null
243-
? $innerParameter->getOutType()
244-
: $innerParameter->getType();
244+
$byRefType = $nodeScopeResolver->resolveByRefParameterType($innerFuncCall, $innerCalleeReflection, $innerParameter, $scope);
245245
$scope = $nodeScopeResolver->processVirtualAssign(
246246
$scope,
247247
$storage,
@@ -250,6 +250,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
250250
new TypeExpr($byRefType),
251251
$nodeCallback,
252252
)->getScope();
253+
$scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $argValue);
253254
}
254255
}
255256
}
@@ -866,6 +867,46 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
866867
return VoidToNullTypeTransformer::transform($parametersAcceptor->getReturnType(), $expr);
867868
}
868869

870+
/**
871+
* @return FunctionReflection|MethodReflection|null
872+
*/
873+
private function resolveCallUserFuncCalleeReflection(FuncCall $innerFuncCall, MutatingScope $scope)
874+
{
875+
if ($innerFuncCall->name instanceof Name && $this->reflectionProvider->hasFunction($innerFuncCall->name, $scope)) {
876+
return $this->reflectionProvider->getFunction($innerFuncCall->name, $scope);
877+
}
878+
879+
if (!$innerFuncCall->name instanceof Expr) {
880+
return null;
881+
}
882+
883+
$callbackType = $scope->getType($innerFuncCall->name);
884+
885+
foreach ($callbackType->getConstantStrings() as $constantString) {
886+
if ($constantString->getValue() === '') {
887+
continue;
888+
}
889+
$funcName = new Name($constantString->getValue());
890+
if ($this->reflectionProvider->hasFunction($funcName, $scope)) {
891+
return $this->reflectionProvider->getFunction($funcName, $scope);
892+
}
893+
}
894+
895+
foreach ($callbackType->getConstantArrays() as $constantArray) {
896+
foreach ($constantArray->findTypeAndMethodNames() as $typeAndMethod) {
897+
if ($typeAndMethod->isUnknown() || !$typeAndMethod->getCertainty()->yes()) {
898+
continue;
899+
}
900+
$methodType = $typeAndMethod->getType();
901+
if ($methodType->hasMethod($typeAndMethod->getMethod())->yes()) {
902+
return $methodType->getMethod($typeAndMethod->getMethod(), $scope);
903+
}
904+
}
905+
}
906+
907+
return null;
908+
}
909+
869910
private function getDynamicFunctionReturnType(MutatingScope $scope, FuncCall $normalizedNode, FunctionReflection $functionReflection): ?Type
870911
{
871912
foreach ($this->dynamicReturnTypeExtensionRegistryProvider->getRegistry()->getDynamicFunctionReturnTypeExtensions($functionReflection) as $dynamicFunctionReturnTypeExtension) {

src/Analyser/NodeScopeResolver.php

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3475,27 +3475,7 @@ public function processArgs(
34753475

34763476
$argValue = $arg->value;
34773477
if (!$argValue instanceof Variable || $argValue->name !== 'this') {
3478-
$paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope);
3479-
if ($paramOutType !== null) {
3480-
$byRefType = $paramOutType;
3481-
} elseif (
3482-
$currentParameter instanceof ExtendedParameterReflection
3483-
&& $currentParameter->getOutType() !== null
3484-
) {
3485-
$byRefType = $currentParameter->getOutType();
3486-
} elseif (
3487-
$calleeReflection instanceof MethodReflection
3488-
&& !$calleeReflection->getDeclaringClass()->isBuiltin()
3489-
) {
3490-
$byRefType = $currentParameter->getType();
3491-
} elseif (
3492-
$calleeReflection instanceof FunctionReflection
3493-
&& !$calleeReflection->isBuiltin()
3494-
) {
3495-
$byRefType = $currentParameter->getType();
3496-
} else {
3497-
$byRefType = new MixedType();
3498-
}
3478+
$byRefType = $this->resolveByRefParameterType($callLike, $calleeReflection, $currentParameter, $scope);
34993479

35003480
$scope = $this->processVirtualAssign(
35013481
$scope,
@@ -3605,6 +3585,40 @@ private function getParameterTypeFromParameterClosureTypeExtension(CallLike $cal
36053585
return null;
36063586
}
36073587

3588+
/**
3589+
* @param MethodReflection|FunctionReflection|null $calleeReflection
3590+
*/
3591+
public function resolveByRefParameterType(CallLike $callLike, $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): Type
3592+
{
3593+
$paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope);
3594+
if ($paramOutType !== null) {
3595+
return $paramOutType;
3596+
}
3597+
3598+
if (
3599+
$currentParameter instanceof ExtendedParameterReflection
3600+
&& $currentParameter->getOutType() !== null
3601+
) {
3602+
return $currentParameter->getOutType();
3603+
}
3604+
3605+
if (
3606+
$calleeReflection instanceof MethodReflection
3607+
&& !$calleeReflection->getDeclaringClass()->isBuiltin()
3608+
) {
3609+
return $currentParameter->getType();
3610+
}
3611+
3612+
if (
3613+
$calleeReflection instanceof FunctionReflection
3614+
&& !$calleeReflection->isBuiltin()
3615+
) {
3616+
return $currentParameter->getType();
3617+
}
3618+
3619+
return new MixedType();
3620+
}
3621+
36083622
/**
36093623
* @param MethodReflection|FunctionReflection|null $calleeReflection
36103624
*/

0 commit comments

Comments
 (0)