From 4bdda6d2b7ffe2e59560c59c894bc5be34e818b9 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:00:40 +0000 Subject: [PATCH 1/2] Validate callback signatures for curl_setopt callback options like CURLOPT_PREREQFUNCTION - Map curl callback options (CURLOPT_WRITEFUNCTION, CURLOPT_HEADERFUNCTION, CURLOPT_READFUNCTION, CURLOPT_PROGRESSFUNCTION, CURLOPT_XFERINFOFUNCTION and CURLOPT_PREREQFUNCTION) to precise `(callable(...): ...)|null` value types in ParametersAcceptorSelector::getCurlOptValueType(). - The callback's first parameter is typed as `CurlHandle` (PHP 8+) or `resource`, followed by the option-specific parameters, and the expected return type (`int` for the write/header/progress/xferinfo/prereq callbacks, `string` for the read callback). Passing a callback with a wrong return type (e.g. returning `bool`) now triggers an argument.type error. - `null` is accepted so resetting a callback keeps working. - The same value types flow through the curl_setopt_array path, so callbacks supplied via the options array are validated identically. --- src/Reflection/ParametersAcceptorSelector.php | 29 ++++++++++ .../CallToFunctionParametersRuleTest.php | 42 ++++++++++++++ .../Functions/data/curl-setopt-array.php | 9 +++ .../Functions/data/curl_setopt_callback.php | 57 +++++++++++++++++++ .../Functions/data/curl_setopt_prereq.php | 28 +++++++++ 5 files changed, 165 insertions(+) create mode 100644 tests/PHPStan/Rules/Functions/data/curl_setopt_callback.php create mode 100644 tests/PHPStan/Rules/Functions/data/curl_setopt_prereq.php diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index f0e50750602..24751d5f65f 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -1250,6 +1250,35 @@ private static function getCurlOptValueType(int $curlOpt): ?Type return $shareType; } + $curlHandleType = PhpVersionStaticAccessor::getInstance()->supportsCurlShareHandle() + ? new ObjectType('CurlHandle') + : new ResourceType(); + + // callback options: [parameter types passed to the callback (after the handle), expected return type] + $callbackConstants = [ + 'CURLOPT_WRITEFUNCTION' => [[new StringType()], new IntegerType()], + 'CURLOPT_HEADERFUNCTION' => [[new StringType()], new IntegerType()], + 'CURLOPT_READFUNCTION' => [[new ResourceType(), new IntegerType()], new StringType()], + 'CURLOPT_PROGRESSFUNCTION' => [[new IntegerType(), new IntegerType(), new IntegerType(), new IntegerType()], new IntegerType()], + 'CURLOPT_XFERINFOFUNCTION' => [[new IntegerType(), new IntegerType(), new IntegerType(), new IntegerType()], new IntegerType()], + 'CURLOPT_PREREQFUNCTION' => [[new StringType(), new StringType(), new IntegerType(), new IntegerType()], new IntegerType()], + ]; + foreach ($callbackConstants as $constName => [$paramTypes, $returnType]) { + if (defined($constName) && constant($constName) === $curlOpt) { + $parameters = [ + new DummyParameter('handle', $curlHandleType, false, PassedByReference::createNo(), false, null), + ]; + foreach ($paramTypes as $i => $paramType) { + $parameters[] = new DummyParameter('param' . $i, $paramType, false, PassedByReference::createNo(), false, null); + } + + return new UnionType([ + new CallableType($parameters, $returnType, false), + new NullType(), + ]); + } + } + // unknown constant return null; } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 70b23c9688e..3d015128329 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1440,6 +1440,43 @@ public function testCurlSetOpt(): void ]); } + public function testCurlSetOptCallback(): void + { + $this->analyse([__DIR__ . '/data/curl_setopt_callback.php'], [ + [ + 'Parameter #3 $value of function curl_setopt expects (callable(CurlHandle, string): int)|null, Closure(mixed, string): true given.', + 13, + ], + [ + 'Parameter #3 $value of function curl_setopt expects (callable(CurlHandle, string): int)|null, Closure(mixed, string): true given.', + 17, + ], + [ + 'Parameter #3 $value of function curl_setopt expects (callable(CurlHandle, resource, int): string)|null, Closure(mixed, mixed, int): 0 given.', + 21, + ], + [ + 'Parameter #3 $value of function curl_setopt expects (callable(CurlHandle, int, int, int, int): int)|null, Closure(mixed, int, int, int, int): false given.', + 25, + ], + [ + 'Parameter #3 $value of function curl_setopt expects (callable(CurlHandle, int, int, int, int): int)|null, Closure(mixed, int, int, int, int): \'\' given.', + 29, + ], + ]); + } + + #[RequiresPhp('>= 8.4.0')] + public function testCurlSetOptPrereqCallback(): void + { + $this->analyse([__DIR__ . '/data/curl_setopt_prereq.php'], [ + [ + 'Parameter #3 $value of function curl_setopt expects (callable(CurlHandle, string, string, int, int): int)|null, Closure(mixed, string, string, int, int): true given.', + 13, + ], + ]); + } + public function testCurlSetOptInvalidShare(): void { if (PHP_VERSION_ID < 80000) { @@ -1478,6 +1515,11 @@ public function testCurlSetOptArray(): void 54, 'Offset 19913 (bool) does not accept type string.', ], + [ + 'Parameter #2 $options of function curl_setopt_array expects array{20011: (callable(CurlHandle, string): int)|null}, array{20011: Closure(mixed, string): true} given.', + 70, + 'Offset 20011 ((callable(CurlHandle, string): int)|null) does not accept type Closure(mixed, string): true.', + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/curl-setopt-array.php b/tests/PHPStan/Rules/Functions/data/curl-setopt-array.php index c6a7639f428..4ab1ac5ad56 100644 --- a/tests/PHPStan/Rules/Functions/data/curl-setopt-array.php +++ b/tests/PHPStan/Rules/Functions/data/curl-setopt-array.php @@ -65,3 +65,12 @@ function doBar($options) { curl_setopt_array($curl, $options); } +function callbackOption() { + $curl = curl_init(); + curl_setopt_array($curl, [ + \CURLOPT_WRITEFUNCTION => function ($ch, string $data): bool { // invalid, must return int + return true; + }, + ]); +} + diff --git a/tests/PHPStan/Rules/Functions/data/curl_setopt_callback.php b/tests/PHPStan/Rules/Functions/data/curl_setopt_callback.php new file mode 100644 index 00000000000..dd942648b15 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/curl_setopt_callback.php @@ -0,0 +1,57 @@ + Date: Tue, 14 Jul 2026 22:48:41 +0200 Subject: [PATCH 2/2] Update CallToFunctionParametersRuleTest.php --- .../PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 3d015128329..44bc0a56d08 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1440,6 +1440,7 @@ public function testCurlSetOpt(): void ]); } + #[RequiresPhp('>= 8.2.0')] public function testCurlSetOptCallback(): void { $this->analyse([__DIR__ . '/data/curl_setopt_callback.php'], [