From fc3200aa302fba55e07ef0b3e3a0c2c05efa49da Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:10:29 +0000 Subject: [PATCH] Do not attach the positional-arguments fix to the callback call synthesized from `call_user_func()` / `call_user_func_array()` - CallUserFuncRule checks a FuncCall built by ArgumentsNormalizer. That node keeps the start/end positions of the original call_user_func(_array)() call, but its arguments are different: the callback argument is dropped, or the arguments come from the array items. - ReplacingNodeVisitor matches nodes by class and position, so the fix for a named argument under @no-named-arguments was applied to the real call_user_func(_array)() node. Its argument indexes did not match, which crashed with "clone(): Argument #1 ($object) must be of type object, null given" (or quietly rewrote call_user_func() into a broken call). - CallUserFuncRule now marks the synthesized call with FunctionCallParametersCheck::NOT_FIXABLE_CALL_ATTRIBUTE, and createPositionalArgumentsFix() does not create a fix for such calls, like it already does for attribute calls. The error is still reported. - Also checked the other FunctionCallParametersCheck::check() callers (CallToFunctionParametersRule, CallMethodsRule, CallStaticMethodsRule, InstantiationRule, CallCallablesRule): they pass real nodes from the file. AttributesCheck already opts out with `isAttribute`. The positional-arguments fix is the only fix this check attaches. --- src/Rules/FunctionCallParametersCheck.php | 11 +++++- src/Rules/Functions/CallUserFuncRule.php | 1 + .../Rules/Functions/CallUserFuncRuleTest.php | 6 +++ .../Rules/Functions/data/bug-15664.php | 38 +++++++++++++++++++ .../Rules/Functions/data/bug-15664.php.fixed | 38 +++++++++++++++++++ 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-15664.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-15664.php.fixed diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index f57854da642..74f61a13449 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -56,6 +56,12 @@ final class FunctionCallParametersCheck { + /** + * Marks a call node that was synthesized from a different node (e.g. the callback call of call_user_func()), + * its arguments do not correspond to the arguments of the node in the file, so it cannot be fixed. + */ + public const NOT_FIXABLE_CALL_ATTRIBUTE = 'phpstanNotFixableCall'; + public function __construct( private RuleLevelHelper $ruleLevelHelper, private NullsafeCheck $nullsafeCheck, @@ -911,8 +917,11 @@ private function createPositionalArgumentsFix( Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $funcCall, ): Closure|false { - if ((bool) $funcCall->getAttribute('isAttribute', false)) { + if ( // the call was created from an attribute, it is not in the file + (bool) $funcCall->getAttribute('isAttribute', false) + || (bool) $funcCall->getAttribute(self::NOT_FIXABLE_CALL_ATTRIBUTE, false) + ) { return false; } diff --git a/src/Rules/Functions/CallUserFuncRule.php b/src/Rules/Functions/CallUserFuncRule.php index 26ae5870806..43ce48fbe02 100644 --- a/src/Rules/Functions/CallUserFuncRule.php +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -69,6 +69,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE return []; } [$parametersAcceptor, $funcCall, $acceptsNamedArguments] = $result; + $funcCall->setAttribute(FunctionCallParametersCheck::NOT_FIXABLE_CALL_ATTRIBUTE, true); $callableDescription = sprintf('callable passed to %s()', $functionName); diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index 02169d40c28..b8a30f27dc7 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -202,4 +202,10 @@ public function testConstantParameterCheckCallUserFunc(): void ]); } + #[RequiresPhp('>= 8.0.0')] + public function testBug15664(): void + { + $this->fix(__DIR__ . '/data/bug-15664.php', __DIR__ . '/data/bug-15664.php.fixed'); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-15664.php b/tests/PHPStan/Rules/Functions/data/bug-15664.php new file mode 100644 index 00000000000..1270ccef74e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-15664.php @@ -0,0 +1,38 @@ += 8.0 + +namespace Bug15664; + +/** + * @no-named-arguments + */ +final class ParallelConfigFactory +{ + + public static function detect( + ?int $filesPerProcess = null, + ?int $processTimeout = null, + ?int $maxProcesses = null + ): void + { + } + +} + +function doFoo(): void +{ + \call_user_func_array([ParallelConfigFactory::class, 'detect'], ['processTimeout' => 300]); + + \call_user_func_array([ParallelConfigFactory::class, 'detect'], [ + 'maxProcesses' => 1, + 'processTimeout' => 300, + 'filesPerProcess' => 5, + ]); + + \call_user_func_array([ParallelConfigFactory::class, 'detect'], ['filesPerProcess' => 7]); + + \call_user_func_array([ParallelConfigFactory::class, 'detect'], ['maxProcesses' => 1]); + + \call_user_func([ParallelConfigFactory::class, 'detect'], processTimeout: 300); + + \call_user_func([ParallelConfigFactory::class, 'detect'], filesPerProcess: 7); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-15664.php.fixed b/tests/PHPStan/Rules/Functions/data/bug-15664.php.fixed new file mode 100644 index 00000000000..1270ccef74e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-15664.php.fixed @@ -0,0 +1,38 @@ += 8.0 + +namespace Bug15664; + +/** + * @no-named-arguments + */ +final class ParallelConfigFactory +{ + + public static function detect( + ?int $filesPerProcess = null, + ?int $processTimeout = null, + ?int $maxProcesses = null + ): void + { + } + +} + +function doFoo(): void +{ + \call_user_func_array([ParallelConfigFactory::class, 'detect'], ['processTimeout' => 300]); + + \call_user_func_array([ParallelConfigFactory::class, 'detect'], [ + 'maxProcesses' => 1, + 'processTimeout' => 300, + 'filesPerProcess' => 5, + ]); + + \call_user_func_array([ParallelConfigFactory::class, 'detect'], ['filesPerProcess' => 7]); + + \call_user_func_array([ParallelConfigFactory::class, 'detect'], ['maxProcesses' => 1]); + + \call_user_func([ParallelConfigFactory::class, 'detect'], processTimeout: 300); + + \call_user_func([ParallelConfigFactory::class, 'detect'], filesPerProcess: 7); +}