Bug report
With bleeding edge (unusedParameters), PHPStan 2.3.0 reports method.unusedParameter on a private method that is handed out as a callable — through a first-class callable (self::plus(...)) or Closure::fromCallable([self::class, 'minus']). Once the method escapes the class that way, its callers are outside the class and pass every argument, so its signature is a contract like a public method's: here an expression engine calls every registered function as $fn($context, ...$args), and most functions ignore $context.
UnusedMethodParametersRule documents that it only checks private methods because "a private method has no callers outside the class, so its signature is not a contract"; a private method turned into a callable breaks that premise. Closures with the same unused parameter (static fn (string $context, int $a): int => $a) are not reported.
Code snippet that reproduces the problem
<?php declare(strict_types = 1);
final class Operators
{
/** @return array<string, Closure(string, int, int): int> */
public static function all(): array
{
return [
'+' => self::plus(...),
'-' => Closure::fromCallable([self::class, 'minus']),
];
}
private static function plus(string $context, int $a, int $b): int
{
return $a + $b;
}
private static function minus(string $context, int $a, int $b): int
{
return $a - $b;
}
}
foreach (Operators::all() as $operator) {
echo $operator('some context', 1, 2);
}
Configuration: level 4 with conf/bleedingEdge.neon included.
repro.php:14 Method Operators::plus() has an unused parameter $context. [method.unusedParameter]
repro.php:19 Method Operators::minus() has an unused parameter $context. [method.unusedParameter]
Expected output
No error: a private method referenced as a callable (first-class callable syntax, or a callable array/string naming it) should be treated like a public one by UnusedMethodParametersRule.
Did PHPStan help you today? Did it make you happy in any way?
Yes — 2.3.0's unused-variable detection found real dead code across our codebase, including a test that never checked what it was meant to.
Bug report
With bleeding edge (
unusedParameters), PHPStan 2.3.0 reportsmethod.unusedParameteron a private method that is handed out as a callable — through a first-class callable (self::plus(...)) orClosure::fromCallable([self::class, 'minus']). Once the method escapes the class that way, its callers are outside the class and pass every argument, so its signature is a contract like a public method's: here an expression engine calls every registered function as$fn($context, ...$args), and most functions ignore$context.UnusedMethodParametersRuledocuments that it only checks private methods because "a private method has no callers outside the class, so its signature is not a contract"; a private method turned into a callable breaks that premise. Closures with the same unused parameter (static fn (string $context, int $a): int => $a) are not reported.Code snippet that reproduces the problem
Configuration: level 4 with
conf/bleedingEdge.neonincluded.Expected output
No error: a private method referenced as a callable (first-class callable syntax, or a callable array/string naming it) should be treated like a public one by
UnusedMethodParametersRule.Did PHPStan help you today? Did it make you happy in any way?
Yes — 2.3.0's unused-variable detection found real dead code across our codebase, including a test that never checked what it was meant to.