diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index f6f9d7778a..d892a5d880 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -14,3 +14,4 @@ parameters: rawMessageInBaseline: true reportNestedTooWideType: false assignToByRefForeachExpr: true + curlSetOptArrayTypes: true diff --git a/conf/config.neon b/conf/config.neon index c3b634f673..801fba87a9 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -38,6 +38,7 @@ parameters: rawMessageInBaseline: false reportNestedTooWideType: false assignToByRefForeachExpr: false + curlSetOptArrayTypes: false fileExtensions: - php checkAdvancedIsset: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 001603aacd..a051de93fc 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -41,6 +41,7 @@ parametersSchema: rawMessageInBaseline: bool() reportNestedTooWideType: bool() assignToByRefForeachExpr: bool() + curlSetOptArrayTypes: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Parser/CurlSetOptArrayArgVisitor.php b/src/Parser/CurlSetOptArrayArgVisitor.php new file mode 100644 index 0000000000..d76e9ea377 --- /dev/null +++ b/src/Parser/CurlSetOptArrayArgVisitor.php @@ -0,0 +1,31 @@ +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; + } + +} diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 6b47480d73..75d6fe864b 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -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; @@ -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; @@ -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; @@ -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); diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index c3dc4df606..26b717e627 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -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'], []); diff --git a/tests/PHPStan/Rules/Functions/data/curl-setopt-array.php b/tests/PHPStan/Rules/Functions/data/curl-setopt-array.php new file mode 100644 index 0000000000..c6a7639f42 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/curl-setopt-array.php @@ -0,0 +1,67 @@ += 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); +} +