diff --git a/src/Framework/Assert.php b/src/Framework/Assert.php index e76aef1a086..7188f2d43a8 100644 --- a/src/Framework/Assert.php +++ b/src/Framework/Assert.php @@ -2534,6 +2534,13 @@ public static function isTrue(): IsTrue return new IsTrue; } + /** + * @psalm-template CallbackInput of mixed + * + * @psalm-param callable(mixed $callback): bool $callback + * + * @psalm-return Callback + */ public static function callback(callable $callback): Callback { return new Callback($callback); diff --git a/src/Framework/Constraint/Callback.php b/src/Framework/Constraint/Callback.php index aa928c041b6..b7cf95a1286 100644 --- a/src/Framework/Constraint/Callback.php +++ b/src/Framework/Constraint/Callback.php @@ -9,18 +9,21 @@ */ namespace PHPUnit\Framework\Constraint; -use function call_user_func; - /** + * @psalm-template CallbackInput of mixed + * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Callback extends Constraint { /** * @var callable + * + * @psalm-var callable(CallbackInput $input): bool */ private $callback; + /** @psalm-param callable(CallbackInput $input): bool $callback */ public function __construct(callable $callback) { $this->callback = $callback; @@ -39,9 +42,11 @@ public function toString(): string * constraint is met, false otherwise. * * @param mixed $other value or object to evaluate + * + * @psalm-param CallbackInput $other */ protected function matches($other): bool { - return call_user_func($this->callback, $other); + return ($this->callback)($other); } } diff --git a/tests/static-analysis/TestUsingCallbacks.php b/tests/static-analysis/TestUsingCallbacks.php new file mode 100644 index 00000000000..2bbc2f85eca --- /dev/null +++ b/tests/static-analysis/TestUsingCallbacks.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\StaticAnalysis; + +use PHPUnit\Framework\TestCase; + +/** @see https://www.youtube.com/watch?v=rXwMrBb2x1Q */ +interface SayHello +{ + public function hey(string $toPerson): string; +} + +/** @small */ +final class TestUsingCallbacks extends TestCase +{ + public function testWillSayHelloAndCheckCallbackInput(): void + { + $mock = $this->createMock(SayHello::class); + + $mock + ->expects(self::once()) + ->method('hey') + ->with(self::callback(static function (string $input): bool { + self::assertStringContainsString('Joe', $input); + + return true; + })) + ->willReturn('Hey Joe!'); + + self::assertSame('Hey Joe!', $mock->hey('Joe')); + } + + public function testWillSayHelloAndCheckCallbackWithoutAnyInput(): void + { + $mock = $this->createMock(SayHello::class); + + $mock + ->expects(self::once()) + ->method('hey') + ->with(self::callback(static function (): bool { + return true; + })) + ->willReturn('Hey Joe!'); + + self::assertSame('Hey Joe!', $mock->hey('Joe')); + } +}