From bc780dafb2fc74670f2f9c6e54795e3f5073dcd4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 1 Oct 2025 14:24:42 +0200 Subject: [PATCH 1/4] Validate curl_setopt_array parameter array --- src/Parser/CurlSetOptArrayArgVisitor.php | 31 +++++++++ src/Reflection/ParametersAcceptorSelector.php | 51 ++++++++++++++ .../CallToFunctionParametersRuleTest.php | 22 ++++++ .../Functions/data/curl-setopt-array.php | 67 +++++++++++++++++++ 4 files changed, 171 insertions(+) create mode 100644 src/Parser/CurlSetOptArrayArgVisitor.php create mode 100644 tests/PHPStan/Rules/Functions/data/curl-setopt-array.php 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..1f880c0c79 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,8 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; @@ -156,6 +159,54 @@ public static function selectFromArgs( } } + if (count($args) >= 2 && (bool) $args[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME)) { + $optArrayType = $scope->getType($args[1]->value); + + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach($optArrayType->getIterableKeyType()->getConstantScalarValues() as $optValue) { + if (!is_int($optValue)) { + $builder = null; + break; + } + + $optValueType = self::getCurlOptValueType($optValue); + if ($optValueType === null) { + $builder = null; + break; + } + + $builder->setOffsetValueType( + new ConstantIntegerType($optValue), + $optValueType + ); + } + + if ($builder !== null) { + $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..122eaf319f 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1378,6 +1378,28 @@ public function testCurlSetOpt(): void ]); } + 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); +} + From d755d5977d8f39c56548464f0c7315c1814f93ed Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 1 Oct 2025 14:25:59 +0200 Subject: [PATCH 2/4] bleeding edge only --- conf/bleedingEdge.neon | 1 + conf/config.neon | 1 + conf/parametersSchema.neon | 1 + src/Reflection/ParametersAcceptorSelector.php | 6 +++--- .../Rules/Functions/CallToFunctionParametersRuleTest.php | 6 +++--- 5 files changed, 9 insertions(+), 6 deletions(-) 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/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 1f880c0c79..7f8ff08768 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -27,7 +27,6 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Generic\TemplateType; @@ -53,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; @@ -163,7 +163,7 @@ public static function selectFromArgs( $optArrayType = $scope->getType($args[1]->value); $builder = ConstantArrayTypeBuilder::createEmpty(); - foreach($optArrayType->getIterableKeyType()->getConstantScalarValues() as $optValue) { + foreach ($optArrayType->getIterableKeyType()->getConstantScalarValues() as $optValue) { if (!is_int($optValue)) { $builder = null; break; @@ -177,7 +177,7 @@ public static function selectFromArgs( $builder->setOffsetValueType( new ConstantIntegerType($optValue), - $optValueType + $optValueType, ); } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 122eaf319f..7285dfc61b 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1384,17 +1384,17 @@ public function testCurlSetOptArray(): void [ "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." + '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." + '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." + 'Offset 19913 (bool) does not accept type string.', ], ]); From 9feb4d0452a1bc2b53c7695a4690cada7eb748c6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 1 Oct 2025 14:37:29 +0200 Subject: [PATCH 3/4] 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 7285dfc61b..26b717e627 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1378,6 +1378,7 @@ public function testCurlSetOpt(): void ]); } + #[RequiresPhp('>= 8.1')] public function testCurlSetOptArray(): void { $this->analyse([__DIR__ . '/data/curl-setopt-array.php'], [ From 0ca00d80e7ddc66bbb9697f849e6b856e4f75b5c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 1 Oct 2025 18:55:41 +0200 Subject: [PATCH 4/4] Update ParametersAcceptorSelector.php --- src/Reflection/ParametersAcceptorSelector.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 7f8ff08768..75d6fe864b 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -162,26 +162,28 @@ 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)) { - $builder = null; + $hasTypes = false; break; } $optValueType = self::getCurlOptValueType($optValue); if ($optValueType === null) { - $builder = null; + $hasTypes = false; break; } + $hasTypes = true; $builder->setOffsetValueType( new ConstantIntegerType($optValue), $optValueType, ); } - if ($builder !== null) { + if ($hasTypes) { $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters();