Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
42 changes: 42 additions & 0 deletions tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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.',
],

]);
}
Expand Down
9 changes: 9 additions & 0 deletions tests/PHPStan/Rules/Functions/data/curl-setopt-array.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
]);
}

57 changes: 57 additions & 0 deletions tests/PHPStan/Rules/Functions/data/curl_setopt_callback.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php declare(strict_types = 1);

namespace CurlSetOptCallback;

class HelloWorld
{

public function invalid(): void
{
$ch = curl_init();

// WRITEFUNCTION expects a callback returning int
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, string $data): bool {
return true;
});
// HEADERFUNCTION expects a callback returning int
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, string $data): bool {
return true;
});
// READFUNCTION expects a callback returning string
curl_setopt($ch, CURLOPT_READFUNCTION, function ($ch, $stream, int $length): int {
return 0;
});
// PROGRESSFUNCTION expects a callback returning int
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function ($ch, int $a, int $b, int $c, int $d): bool {
return false;
});
// XFERINFOFUNCTION expects a callback returning int
curl_setopt($ch, CURLOPT_XFERINFOFUNCTION, function ($ch, int $a, int $b, int $c, int $d): string {
return '';
});
}

public function valid(): void
{
$ch = curl_init();

curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, string $data): int {
return strlen($data);
});
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, string $header): int {
return strlen($header);
});
curl_setopt($ch, CURLOPT_READFUNCTION, function ($ch, $stream, int $length): string {
return '';
});
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function ($ch, int $a, int $b, int $c, int $d): int {
return 0;
});
curl_setopt($ch, CURLOPT_XFERINFOFUNCTION, function ($ch, int $a, int $b, int $c, int $d): int {
return 0;
});
// null resets the callback and is allowed
curl_setopt($ch, CURLOPT_WRITEFUNCTION, null);
}

}
28 changes: 28 additions & 0 deletions tests/PHPStan/Rules/Functions/data/curl_setopt_prereq.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);

namespace CurlSetOptPrereq;

class HelloWorld
{

public function invalid(): void
{
$ch = curl_init();

// CURLOPT_PREREQFUNCTION (PHP 8.4+) expects a callback returning int
curl_setopt($ch, CURLOPT_PREREQFUNCTION, function ($ch, string $primaryIp, string $connectIp, int $primaryPort, int $connectPort): bool {
return true;
});
}

public function valid(): void
{
$ch = curl_init();

curl_setopt($ch, CURLOPT_PREREQFUNCTION, function ($ch, string $primaryIp, string $connectIp, int $primaryPort, int $connectPort): int {
return CURL_PREREQFUNC_OK;
});
curl_setopt($ch, CURLOPT_PREREQFUNCTION, null);
}

}
Loading