From 8294602fe3f97df4191f2b8bf0f0b61b6fcab9b8 Mon Sep 17 00:00:00 2001 From: Michel Roca Date: Tue, 22 Jul 2025 13:44:31 +0200 Subject: [PATCH] feat: infer preg_replace_callback return type --- ...afeFunctionsDynamicReturnTypeExtension.php | 9 +++++++- tests/Type/Php/data/preg_match_checked.php | 4 ++-- tests/Type/Php/data/preg_match_unchecked.php | 4 ++-- tests/Type/Php/data/preg_replace_return.php | 23 +++++++++++++++++++ 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php index 443d1bb..2b81b12 100644 --- a/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php @@ -16,12 +16,19 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +/** + * This file has been copy-pasted from PHPStan's source code but with isFunctionSupported changed. + * For now, only preg_* functions are supported. + * @See https://github.com/phpstan/phpstan-src/blob/2.1.x/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php + */ class ReplaceSafeFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var array */ + /** @var array The function name associated with the typed argument position used to type the return */ private array $functions = [ 'Safe\preg_replace' => 2, + 'Safe\preg_replace_callback' => 2, + 'Safe\preg_replace_callback_array' => 1, ]; public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/tests/Type/Php/data/preg_match_checked.php b/tests/Type/Php/data/preg_match_checked.php index 0ecea9f..0fbb3d9 100644 --- a/tests/Type/Php/data/preg_match_checked.php +++ b/tests/Type/Php/data/preg_match_checked.php @@ -12,9 +12,9 @@ // @phpstan-ignore-next-line - use of unsafe is intentional if(\preg_match($pattern, $string, $matches)) { - \PHPStan\Testing\assertType($type, $matches); + \PHPStan\Testing\assertSuperType($type, $matches); } if(\Safe\preg_match($pattern, $string, $matches)) { - \PHPStan\Testing\assertType($type, $matches); + \PHPStan\Testing\assertSuperType($type, $matches); } diff --git a/tests/Type/Php/data/preg_match_unchecked.php b/tests/Type/Php/data/preg_match_unchecked.php index 8a55990..932cf5c 100644 --- a/tests/Type/Php/data/preg_match_unchecked.php +++ b/tests/Type/Php/data/preg_match_unchecked.php @@ -11,7 +11,7 @@ // @phpstan-ignore-next-line - use of unsafe is intentional \preg_match($pattern, $string, $matches); -\PHPStan\Testing\assertType($type, $matches); +\PHPStan\Testing\assertSuperType($type, $matches); \Safe\preg_match($pattern, $string, $matches); -\PHPStan\Testing\assertType($type, $matches); +\PHPStan\Testing\assertSuperType($type, $matches); diff --git a/tests/Type/Php/data/preg_replace_return.php b/tests/Type/Php/data/preg_replace_return.php index 94d6c8c..c8eba5d 100644 --- a/tests/Type/Php/data/preg_replace_return.php +++ b/tests/Type/Php/data/preg_replace_return.php @@ -2,6 +2,8 @@ namespace TheCodingMachine\Safe\PHPStan\Type\Php\data; +// preg_replace + // preg_replace with a string pattern should return a string $x = \Safe\preg_replace('/foo/', 'bar', 'baz'); \PHPStan\Testing\assertType("string", $x); @@ -9,3 +11,24 @@ // preg_replace with an array pattern should return an array $x = \Safe\preg_replace(['/foo/'], ['bar'], ['baz']); \PHPStan\Testing\assertType("array", $x); + +// preg_replace_callback + +// preg_replace_callback with a string pattern should return a string +$x = \Safe\preg_replace_callback('/foo/', fn (array $matches) => 'bar', 'baz'); +\PHPStan\Testing\assertType("string", $x); + +// preg_replace_callback with an array pattern should return an array +$x = \Safe\preg_replace_callback(['/foo/'], fn (array $matches) => 'bar', ['baz']); +\PHPStan\Testing\assertType("array", $x); + + +// preg_replace_callback_array + +// preg_replace_callback_array with a string pattern should return a string +$x = \Safe\preg_replace_callback_array(['/foo/' => fn (array $matches) => 'bar'], 'baz'); +\PHPStan\Testing\assertType("string", $x); + +// preg_replace_callback with an array pattern should return an array +$x = \Safe\preg_replace_callback_array(['/foo/' => fn (array $matches) => 'bar'], ['baz']); +\PHPStan\Testing\assertType("array", $x);