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
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ parameters:
rawMessageInBaseline: true
reportNestedTooWideType: false
assignToByRefForeachExpr: true
curlSetOptArrayTypes: true
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ parameters:
rawMessageInBaseline: false
reportNestedTooWideType: false
assignToByRefForeachExpr: false
curlSetOptArrayTypes: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ parametersSchema:
rawMessageInBaseline: bool()
reportNestedTooWideType: bool()
assignToByRefForeachExpr: bool()
curlSetOptArrayTypes: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
31 changes: 31 additions & 0 deletions src/Parser/CurlSetOptArrayArgVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);

namespace PHPStan\Parser;

use Override;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PHPStan\DependencyInjection\AutowiredService;

#[AutowiredService]
final class CurlSetOptArrayArgVisitor extends NodeVisitorAbstract
{

public const ATTRIBUTE_NAME = 'isCurlSetOptArrayArg';

#[Override]
public function enterNode(Node $node): ?Node
{
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) {
$functionName = $node->name->toLowerString();
if ($functionName === 'curl_setopt_array') {
$args = $node->getRawArgs();
if (isset($args[1])) {
$args[1]->setAttribute(self::ATTRIBUTE_NAME, true);
}
}
}
return null;
}

}
53 changes: 53 additions & 0 deletions src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use PHPStan\Parser\ClosureBindArgVisitor;
use PHPStan\Parser\ClosureBindToVarVisitor;
use PHPStan\Parser\CurlSetOptArgVisitor;
use PHPStan\Parser\CurlSetOptArrayArgVisitor;
use PHPStan\Parser\ImplodeArgVisitor;
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
use PHPStan\Reflection\Native\NativeParameterReflection;
Expand All @@ -26,6 +27,7 @@
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\CallableType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeMap;
Expand All @@ -50,6 +52,7 @@
use function constant;
use function count;
use function defined;
use function is_int;
use function is_string;
use function sprintf;
use const ARRAY_FILTER_USE_BOTH;
Expand Down Expand Up @@ -156,6 +159,56 @@ public static function selectFromArgs(
}
}

if (count($args) >= 2 && (bool) $args[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME)) {
$optArrayType = $scope->getType($args[1]->value);

$hasTypes = false;
$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($optArrayType->getIterableKeyType()->getConstantScalarValues() as $optValue) {
if (!is_int($optValue)) {
$hasTypes = false;
break;
}

$optValueType = self::getCurlOptValueType($optValue);
if ($optValueType === null) {
$hasTypes = false;
break;
}

$hasTypes = true;
$builder->setOffsetValueType(
new ConstantIntegerType($optValue),
$optValueType,
);
}

if ($hasTypes) {
$acceptor = $parametersAcceptors[0];
$parameters = $acceptor->getParameters();

$parameters[1] = new NativeParameterReflection(
$parameters[1]->getName(),
$parameters[1]->isOptional(),
$builder->getArray(),
$parameters[1]->passedByReference(),
$parameters[1]->isVariadic(),
$parameters[1]->getDefaultValue(),
);

$parametersAcceptors = [
new FunctionVariant(
$acceptor->getTemplateTypeMap(),
$acceptor->getResolvedTemplateTypeMap(),
array_values($parameters),
$acceptor->isVariadic(),
$acceptor->getReturnType(),
$acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
),
];
}
}

if ((bool) $args[0]->getAttribute(ArrayFilterArgVisitor::ATTRIBUTE_NAME)) {
if (isset($args[2])) {
$mode = $scope->getType($args[2]->value);
Expand Down
23 changes: 23 additions & 0 deletions tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,29 @@ public function testCurlSetOpt(): void
]);
}

#[RequiresPhp('>= 8.1')]
public function testCurlSetOptArray(): void
{
$this->analyse([__DIR__ . '/data/curl-setopt-array.php'], [
[
"Parameter #2 \$options of function curl_setopt_array expects array{19913: bool, 10102: string, 68: int, 13: int, 84: int, 10036: non-empty-string|null}, array{19913: true, 10102: '', 68: 10, 13: 30, 84: 2, 10036: CurlSetOptArray\RequestMethod::POST} given.",
30,
'Offset 10036 (non-empty-string|null) does not accept type CurlSetOptArray\RequestMethod::POST.',
],
[
"Parameter #2 \$options of function curl_setopt_array expects array{19913: bool, 10102: string, 68: int, 13: int, 84: int, 10036: non-empty-string|null}, array{19913: true, 10102: '', 68: 10, 13: 30, 84: 2, 10036: CurlSetOptArray\BackedRequestMethod::POST} given.",
42,
'Offset 10036 (non-empty-string|null) does not accept type CurlSetOptArray\BackedRequestMethod::POST.',
],
[
"Parameter #2 \$options of function curl_setopt_array expects array{19913: bool, 10102: string, 68: int, 13: int, 84: int}, array{19913: '123', 10102: '', 68: 10, 13: 30, 84: false} given.",
54,
'Offset 19913 (bool) does not accept type string.',
],

]);
}

public function testBug8280(): void
{
$this->analyse([__DIR__ . '/data/bug-8280.php'], []);
Expand Down
67 changes: 67 additions & 0 deletions tests/PHPStan/Rules/Functions/data/curl-setopt-array.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php // lint >= 8.1

namespace CurlSetOptArray;

enum RequestMethod
{
case GET;
case POST;
}

enum BackedRequestMethod: string
{
case POST = 'POST';
case GET = 'GET';
}

function allOptionsFine() {
$curl = curl_init();
curl_setopt_array($curl, [
\CURLOPT_RETURNTRANSFER => true,
\CURLOPT_ENCODING => '',
\CURLOPT_MAXREDIRS => 10,
\CURLOPT_TIMEOUT => 30,
\CURLOPT_HTTP_VERSION => \CURL_HTTP_VERSION_1_1,
]);
}

function doFoo() {
$curl = curl_init();
curl_setopt_array($curl, [
\CURLOPT_RETURNTRANSFER => true,
\CURLOPT_ENCODING => '',
\CURLOPT_MAXREDIRS => 10,
\CURLOPT_TIMEOUT => 30,
\CURLOPT_HTTP_VERSION => \CURL_HTTP_VERSION_1_1,
\CURLOPT_CUSTOMREQUEST => RequestMethod::POST, // invalid
]);
}

function doFoo2() {
$curl = curl_init();
curl_setopt_array($curl, [
\CURLOPT_RETURNTRANSFER => true,
\CURLOPT_ENCODING => '',
\CURLOPT_MAXREDIRS => 10,
\CURLOPT_TIMEOUT => 30,
\CURLOPT_HTTP_VERSION => \CURL_HTTP_VERSION_1_1,
\CURLOPT_CUSTOMREQUEST => BackedRequestMethod::POST,
]);
}

function doFooBar() {
$curl = curl_init();
curl_setopt_array($curl, [
\CURLOPT_RETURNTRANSFER => '123', // invalid
\CURLOPT_ENCODING => '',
\CURLOPT_MAXREDIRS => 10,
\CURLOPT_TIMEOUT => 30,
\CURLOPT_HTTP_VERSION => false, // invalid
]);
}

function doBar($options) {
$curl = curl_init();
curl_setopt_array($curl, $options);
}

Loading