diff --git a/src/Utils/PartiallyAppliedFunction.php b/src/Utils/PartiallyAppliedFunction.php index 279e5c6..6bf81cc 100644 --- a/src/Utils/PartiallyAppliedFunction.php +++ b/src/Utils/PartiallyAppliedFunction.php @@ -66,17 +66,26 @@ class PartiallyAppliedFunction implements Decorator, Compilable return $this->callable; } + /** + * Internal functions throw when given too many arguments, so for a non-variadic internal target the compiled + * closure drops trailing arguments past its parameter count (e.g. the key passed by collection functions). + */ public function compile(): \Closure { $callable = compile($this->callable); $arguments = $this->arguments; $placeholders = array_keys($arguments, PLACEHOLDER, true); + $target = new \ReflectionFunction($callable); + $extra = $target->isInternal() && !$target->isVariadic() + ? max(0, $target->getNumberOfParameters() - count($arguments)) + : null; + if (empty($placeholders)) { - return static fn (...$args) => $callable(...$arguments, ...array_values($args)); + return static fn (...$args) => $callable(...$arguments, ...array_slice(array_values($args), 0, $extra)); } - return static function (...$args) use ($callable, $arguments, $placeholders) { + return static function (...$args) use ($callable, $arguments, $placeholders, $extra) { $args = array_values($args); $count = min(count($placeholders), count($args)); @@ -84,7 +93,7 @@ class PartiallyAppliedFunction implements Decorator, Compilable $arguments[$placeholders[$i]] = $args[$i]; } - return $callable(...$arguments, ...array_slice($args, $count)); + return $callable(...$arguments, ...array_slice($args, $count, $extra)); }; } diff --git a/tests/Collections/CallbackTest.php b/tests/Collections/CallbackTest.php index 01bc8ce..e0481df 100644 --- a/tests/Collections/CallbackTest.php +++ b/tests/Collections/CallbackTest.php @@ -38,6 +38,11 @@ class CallbackTest extends \PHPUnit\Framework\TestCase $this->assertSame(['v', 'k'], callback(fn (...$args) => $args)('v', 'k')); } + public function testPartialOfInternalFunctionDoesNotGetKey() + { + $this->assertSame('a', callback(f\partial(trim(...), _, 'x'))('xax', 'key')); + } + public function testDecoratorIsCompiled() { $callback = callback(f\partial(fn ($prefix, $v, $k) => "$prefix$k=$v", '>')); diff --git a/tests/CompileTest.php b/tests/CompileTest.php index ab88f4d..1339857 100644 --- a/tests/CompileTest.php +++ b/tests/CompileTest.php @@ -88,6 +88,21 @@ class CompileTest extends \PHPUnit\Framework\TestCase $this->assertSame($partial(), f\compile($partial)()); } + public function testCompileDropsExtraArgumentsForInternalTarget() + { + $this->assertSame('aa', f\compile(f\partial(str_repeat(...), _, 2))('a', 'extra')); + } + + public function testCompileKeepsExtraArgumentsForVariadicInternalTarget() + { + $this->assertSame(['a', 'b', 'c'], f\compile(f\partial(array_merge(...), ['a']))(['b'], ['c'])); + } + + public function testCompileDropsExtraArgumentsForCurriedInternalTarget() + { + $this->assertSame('aa', f\compile(f\curry(str_repeat(...), null, 'a'))(2, 'extra')); + } + public function testCompileFunctionDelegatesToMethod() { $partial = f\partial(str_replace(...), 'a', _, 'banana');