avoid by-reference warnings in filter

This commit is contained in:
Kacper Donat 2026-10-28 09:56:11 +01:00
parent 71e1b2cbdc
commit d013a84f90
2 changed files with 46 additions and 3 deletions

View File

@ -64,13 +64,17 @@ function callback(callable $callable, int $arity = 1): \Closure
$leadingOnly = $target->isInternal() $leadingOnly = $target->isInternal()
|| (!$target->isVariadic() && $target->getNumberOfParameters() === $arity); || (!$target->isVariadic() && $target->getNumberOfParameters() === $arity);
// Internal functions like array_filter() pass values, not variables, so a closure taking a parameter by reference
// would warn on each call. Called from a wrapper, it gets the wrapper's variables instead.
$reflection = new \ReflectionFunction($compiled);
$byReference = takesByReference($reflection);
if (!$leadingOnly) { if (!$leadingOnly) {
return $compiled; return $byReference ? static fn (...$args) => $compiled(...$args) : $compiled;
} }
// A userland closure taking no more than the leading arguments already ignores the rest. // A userland closure taking no more than the leading arguments already ignores the rest.
$reflection = new \ReflectionFunction($compiled); if (!$byReference && !$reflection->isInternal() && !$reflection->isVariadic() && $reflection->getNumberOfParameters() <= $arity) {
if (!$reflection->isInternal() && !$reflection->isVariadic() && $reflection->getNumberOfParameters() <= $arity) {
return $compiled; return $compiled;
} }
@ -79,6 +83,22 @@ function callback(callable $callable, int $arity = 1): \Closure
: static fn ($carry, $value) => $compiled($carry, $value); : static fn ($carry, $value) => $compiled($carry, $value);
} }
/**
* Whether a function takes any of its parameters by reference.
*
* @internal
*/
function takesByReference(\ReflectionFunction $reflection): bool
{
foreach ($reflection->getParameters() as $parameter) {
if ($parameter->isPassedByReference()) {
return true;
}
}
return false;
}
/** /**
* Finds the function a callable ultimately calls: decorators are unwrapped, also when they were turned into * Finds the function a callable ultimately calls: decorators are unwrapped, also when they were turned into
* closures, and a pipeline gives its first function, which gets all of its arguments. * closures, and a pipeline gives its first function, which gets all of its arguments.

View File

@ -107,6 +107,29 @@ class TransformsTest extends \PHPUnit\Framework\TestCase
$this->assertSeq([2], c\filter(fn ($v) => $v % 2 === 0)(self::gen([1, 2, 3]))); $this->assertSeq([2], c\filter(fn ($v) => $v % 2 === 0)(self::gen([1, 2, 3])));
} }
public function testFilterWithByReferenceCallbackDoesNotWarn()
{
$warnings = [];
set_error_handler(function (int $level, string $message) use (&$warnings) {
$warnings[] = $message;
return true;
});
try {
$array = c\filter(fn (&$v) => $v > 1)([1, 2, 3]);
$seq = c\filter(fn (&$v) => $v > 1)(new Seq([1, 2, 3]));
$both = c\filter(fn (&$v, $k) => $k > 0)([1, 2, 3]);
} finally {
restore_error_handler();
}
$this->assertSame([], $warnings);
$this->assertSame([1 => 2, 2 => 3], $array);
$this->assertSeq([2, 3], $seq);
$this->assertSame([1 => 2, 2 => 3], $both);
}
public function testFilterWithInternalPredicate() public function testFilterWithInternalPredicate()
{ {
$this->assertSame([1 => 'a'], c\filter(is_string(...))([1, 'a'])); $this->assertSame([1 => 'a'], c\filter(is_string(...))([1, 'a']));