Fix phpstan/phpstan#6799: wrongly reported empty array#5292
Fix phpstan/phpstan#6799: wrongly reported empty array#5292phpstan-bot wants to merge 15 commits intophpstan:2.1.xfrom
Conversation
- When call_user_func_array (or call_user_func) passes arguments by reference, the scope now properly reflects that the referenced variables may be modified - Added by-reference parameter handling in FuncCallHandler::processExpr that resolves the inner callable's parameters and assigns the parameter type to by-ref argument expressions via processVirtualAssign - New regression test in tests/PHPStan/Analyser/nsrt/bug-6799.php - Root cause: processArgs only handled by-ref for call_user_func_array's own parameters, not the target callable's parameters
VincentLanglet
left a comment
There was a problem hiding this comment.
Add a test which calls a method with a param-out phpdoc, the type of the reference should be updated according the param-out one instead of the param one.
When a by-reference parameter has a @param-out PHPDoc, use that type instead of the @param type when updating scope after call_user_func_array. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Already processed — all tests passed. The fix has been committed and pushed. |
| $byRefType = $innerParameter instanceof ExtendedParameterReflection && $innerParameter->getOutType() !== null | ||
| ? $innerParameter->getOutType() | ||
| : $innerParameter->getType(); |
There was a problem hiding this comment.
There is
if ($currentParameter === null) {
throw new ShouldNotHappenException();
}
$argValue = $arg->value;
if (!$argValue instanceof Variable || $argValue->name !== 'this') {
$paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope);
if ($paramOutType !== null) {
$byRefType = $paramOutType;
} elseif (
$currentParameter instanceof ExtendedParameterReflection
&& $currentParameter->getOutType() !== null
) {
$byRefType = $currentParameter->getOutType();
} elseif (
$calleeReflection instanceof MethodReflection
&& !$calleeReflection->getDeclaringClass()->isBuiltin()
) {
$byRefType = $currentParameter->getType();
} elseif (
$calleeReflection instanceof FunctionReflection
&& !$calleeReflection->isBuiltin()
) {
$byRefType = $currentParameter->getType();
} else {
$byRefType = new MixedType();
}
$scope = $this->processVirtualAssign(
$scope,
$storage,
$stmt,
$argValue,
new TypeExpr($byRefType),
$nodeCallback,
)->getScope();
$scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $argValue);
}
}
in NodeScopeResolver
There is certainly something to refactor and use here.
There was a problem hiding this comment.
Pushed. Here's a summary of the changes addressing VincentLanglet's review:
-
Extracted
resolveByRefParameterType()on NodeScopeResolver - New public method centralizing the by-reference parameter type resolution logic (extension types viagetParameterOutExtensionsType,@param-outviagetOutType(), builtin vs user-defined distinction,MixedTypefallback). Used in bothprocessArgsand FuncCallHandler. -
Added
resolveCallUserFuncCalleeReflection()on FuncCallHandler - Resolves the inner callee reflection from the callback expression: handlesName-based function resolution, constant string function names, and array-based method callables viafindTypeAndMethodNames(). -
Added
lookForUnsetAllowedUndefinedExpressions()call in FuncCallHandler after the virtual assignment, matching the NodeScopeResolver pattern. -
Removed the
ExtendedParameterReflectionimport from FuncCallHandler since that logic now lives inresolveByRefParameterType.
…lver 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>
|
|
||
| $callbackType = $scope->getType($innerFuncCall->name); | ||
|
|
||
| foreach ($callbackType->getConstantStrings() as $constantString) { |
There was a problem hiding this comment.
Those two foreach loops are not correct since they return the first result meaning that a call to 'foo'|'bar' will be considered as a call to 'foo'.
Let's just simplify and support only when there is 1 value.
There was a problem hiding this comment.
Done. The change simplifies resolveCallUserFuncCalleeReflection to only resolve the callee when there's exactly one constant string or one constant array callback value, instead of iterating with foreach loops that incorrectly returned the first match from union types like 'foo'|'bar'.
All tests pass (11656 tests, 78894 assertions) and make phpstan reports no errors.
…alue callbacks Instead of iterating and returning the first match (which incorrectly handles union types like 'foo'|'bar' by only considering 'foo'), only resolve the callee when there is exactly one constant string or one constant array callback value. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VincentLanglet
left a comment
There was a problem hiding this comment.
Add a test for an union of string 'method1'|'method2'
do the same for an union of constantArray.
Also add a test for 'method1'|constantArray
…dling
Tests cover:
- Union of string method names ('method1'|'method2')
- Union of constant array callbacks
- Mixed union of string and array callback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Already handled — all 11656 tests passed. The commit has been pushed. |
- Add native return type to resolveCallUserFuncCalleeReflection - Move $this check to be first inside the loop Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…s check - Add native return type to resolveCallUserFuncCalleeReflection - Add early return null after constant strings block since a type cannot be both constant string and constant array - Move $this check to be first inside the loop Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…type - Lazy-initialize innerCalleeReflection with ??= so reflection work only runs after the passedByReference()->createsNewVariable() check - Use native union parameter type on resolveByRefParameterType Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…parison Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
does not seems to be a good optimisation since this metod might return So in this case you will call it in every step of the loop. Is it ok to put it back before the loop ? Best would be to simplify |
I think its pretty unlikely that we use call_user_func with a function expecting a reference at all. so I think the "optimization" saves more on the general case, because we don't need to resolve reflection for most uses of call_user_func (which don't use references at all). it will only be invoked multiple times if you have a callable which expects more than 1 by-ref param, which is even more unlikely. but sure, there is room for improvement
agree but I don't know yet. will have another look. |
VincentLanglet
left a comment
There was a problem hiding this comment.
The resolveCallUserFuncCalleeReflection method seems complicated and it would be better to avoid it because we have a nicer abstraction for that - CallableParametersAcceptor.
It would be better to add a method to the CallableParametersAcceptor (if needed) and use it here.
…ceptor::getCalleeReflection() Add getCalleeReflection() to CallableParametersAcceptor interface, returning the underlying FunctionReflection or MethodReflection when available. This replaces the manual callable resolution logic in FuncCallHandler with the existing callable type abstraction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
That was the earlier |
Summary
When using
call_user_func_arraywith by-reference arguments (e.g.,[&$whereFilter, ...]), PHPStan did not recognize that the referenced variables could be modified by the called function. This caused false positives likeComparison operation ">" between 0 and 0 is always falsewhen checkingcount($whereFilter) > 0after the call.Changes
call_user_func_arrayandcall_user_funcinsrc/Analyser/ExprHandler/FuncCallHandler.phpcall_user_func_arrayargs, the fix resolves the inner callable's parameters viaArgumentsNormalizer::reorderCallUserFuncArrayArgumentsand assigns the parameter type to any by-reference argument expressions usingprocessVirtualAssigntests/PHPStan/Analyser/nsrt/bug-6799.phpRoot cause
FuncCallHandler::processExprcalledprocessArgswithcall_user_func_array's own parameter signatures. Sincecall_user_func_arrayitself doesn't have by-reference parameters, the by-reference handling inprocessArgsnever triggered for the target callable's parameters. The variables passed with&inside the args array were never updated in the scope, so they retained their original types (e.g.,array{}for an initially empty array).Test
The regression test creates a class with a method that modifies an array by reference, then calls that method via
call_user_func_arraywith&$whereFilter. It asserts that$whereFilterhas typearray<string>after the call, notarray{}.Fixes phpstan/phpstan#6799