diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index f0e5075060..24751d5f65 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 70b23c9688..44bc0a56d0 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1440,6 +1440,44 @@ public function testCurlSetOpt(): void ]); } + #[RequiresPhp('>= 8.2.0')] + 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 +1516,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 c6a7639f42..4ab1ac5ad5 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 0000000000..dd942648b1 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/curl_setopt_callback.php @@ -0,0 +1,57 @@ +