Skip to content

Commit 2d47da6

Browse files
committed
Fix handling of multiple constant values
1 parent 21984c2 commit 2d47da6

File tree

5 files changed

+209
-77
lines changed

5 files changed

+209
-77
lines changed

src/Type/Php/CurlInitReturnTypeExtension.php

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111
use PHPStan\Type\Constant\ConstantBooleanType;
1212
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1313
use PHPStan\Type\NeverType;
14+
use PHPStan\Type\NullType;
15+
use PHPStan\Type\StringType;
1416
use PHPStan\Type\Type;
1517
use PHPStan\Type\TypeCombinator;
18+
use PHPStan\Type\UnionType;
19+
use function array_map;
1620
use function count;
1721
use function is_string;
1822
use function parse_url;
@@ -50,43 +54,49 @@ public function getTypeFromFunctionCall(
5054
}
5155

5256
$urlArgType = $scope->getType($args[0]->value);
53-
if ($urlArgType->isNull()->yes()) {
57+
if ($urlArgType->isConstantScalarValue()->yes() && (new UnionType([new NullType(), new StringType()]))->isSuperTypeOf($urlArgType)->yes()) {
58+
$urlArgReturnTypes = array_map(
59+
fn ($value) => $this->getUrlArgValueReturnType($value, $returnType, $notFalseReturnType),
60+
$urlArgType->getConstantScalarValues(),
61+
);
62+
return TypeCombinator::union(...$urlArgReturnTypes);
63+
}
64+
65+
return $returnType;
66+
}
67+
68+
private function getUrlArgValueReturnType(mixed $urlArgValue, Type $returnType, Type $notFalseReturnType): Type
69+
{
70+
if ($urlArgValue === null) {
5471
return $notFalseReturnType;
5572
}
56-
if ($urlArgType->isString()->yes()) {
57-
$urlArgValue = $urlArgType->getConstantScalarValues()[0] ?? null;
58-
if ($urlArgValue !== null) {
59-
if (!is_string($urlArgValue)) {
60-
throw new ShouldNotHappenException();
61-
}
62-
if (str_contains($urlArgValue, "\0")) {
63-
if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) {
64-
// https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L112-L115
65-
return new ConstantBooleanType(false);
66-
}
67-
// https://github.com/php/php-src/blob/php-8.0.0/ext/curl/interface.c#L104-L107
68-
return new NeverType();
69-
}
70-
if ($this->phpVersion->getVersionId() < 80000) {
71-
// Before PHP 8.0 an unparsable URL or a file:// scheme would fail if open_basedir is used
72-
// Since we can't detect open_basedir properly, we'll always consider a failure possible if these
73-
// conditions are given
74-
// https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L139-L158
75-
$parsedUrlArg = parse_url($urlArgValue);
76-
if ($parsedUrlArg === false || (isset($parsedUrlArg['scheme']) && strcasecmp($parsedUrlArg['scheme'], 'file') === 0)) {
77-
return $returnType;
78-
}
79-
}
80-
if (strlen($urlArgValue) > self::CURL_MAX_INPUT_LENGTH) {
81-
// Since libcurl 7.65.0 this would always fail, but no current PHP version requires it at the moment
82-
// https://github.com/curl/curl/commit/5fc28510a4664f46459d9a40187d81cc08571e60
83-
return $returnType;
84-
}
85-
return $notFalseReturnType;
73+
if (!is_string($urlArgValue)) {
74+
throw new ShouldNotHappenException();
75+
}
76+
if (str_contains($urlArgValue, "\0")) {
77+
if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) {
78+
// https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L112-L115
79+
return new ConstantBooleanType(false);
8680
}
81+
// https://github.com/php/php-src/blob/php-8.0.0/ext/curl/interface.c#L104-L107
82+
return new NeverType();
8783
}
88-
89-
return $returnType;
84+
if ($this->phpVersion->getVersionId() < 80000) {
85+
// Before PHP 8.0 an unparsable URL or a file:// scheme would fail if open_basedir is used
86+
// Since we can't detect open_basedir properly, we'll always consider a failure possible if these
87+
// conditions are given
88+
// https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L139-L158
89+
$parsedUrlArgValue = parse_url($urlArgValue);
90+
if ($parsedUrlArgValue === false || (isset($parsedUrlArgValue['scheme']) && strcasecmp($parsedUrlArgValue['scheme'], 'file') === 0)) {
91+
return $returnType;
92+
}
93+
}
94+
if (strlen($urlArgValue) > self::CURL_MAX_INPUT_LENGTH) {
95+
// Since libcurl 7.65.0 this would always fail, but no current PHP version requires it at the moment
96+
// https://github.com/curl/curl/commit/5fc28510a4664f46459d9a40187d81cc08571e60
97+
return $returnType;
98+
}
99+
return $notFalseReturnType;
90100
}
91101

92102
}

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3079,50 +3079,6 @@ public function dataBinaryOperations(): array
30793079
'bool',
30803080
'array_key_exists(\'foo\', $generalArray)',
30813081
],
3082-
[
3083-
PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle',
3084-
'curl_init()',
3085-
],
3086-
[
3087-
PHP_VERSION_ID < 80000 ? 'resource|false' : 'CurlHandle|false',
3088-
'curl_init($string)',
3089-
],
3090-
[
3091-
PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle',
3092-
'curl_init(null)',
3093-
],
3094-
[
3095-
PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle',
3096-
'curl_init(\'https://phpstan.org\')',
3097-
],
3098-
[
3099-
PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*',
3100-
"curl_init('\0')",
3101-
],
3102-
[
3103-
PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*',
3104-
"curl_init('https://phpstan.org\0')",
3105-
],
3106-
[
3107-
PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle',
3108-
'curl_init(\'\')',
3109-
],
3110-
[
3111-
PHP_VERSION_ID < 80000 ? 'resource|false' : 'CurlHandle',
3112-
'curl_init(\':\')',
3113-
],
3114-
[
3115-
PHP_VERSION_ID < 80000 ? 'resource|false' : 'CurlHandle',
3116-
'curl_init(\'file://host/text.txt\')',
3117-
],
3118-
[
3119-
PHP_VERSION_ID < 80000 ? 'resource|false' : 'CurlHandle',
3120-
'curl_init(\'FIle://host/text.txt\')',
3121-
],
3122-
[
3123-
PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle',
3124-
'curl_init(\'host/text.txt\')',
3125-
],
31263082
[
31273083
'string',
31283084
'sprintf($string, $string, 1)',
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
use const PHP_VERSION_ID;
7+
8+
class CurlInitReturnTypeExtensionTest extends TypeInferenceTestCase
9+
{
10+
11+
public static function dataFileAsserts(): iterable
12+
{
13+
if (PHP_VERSION_ID < 80000) {
14+
yield from self::gatherAssertTypes(__DIR__ . '/data/curl-init-php-7.php');
15+
} else {
16+
yield from self::gatherAssertTypes(__DIR__ . '/data/curl-init-php-8.php');
17+
}
18+
}
19+
20+
/**
21+
* @dataProvider dataFileAsserts
22+
*/
23+
public function testFileAsserts(
24+
string $assertType,
25+
string $file,
26+
mixed ...$args,
27+
): void
28+
{
29+
$this->assertFileAsserts($assertType, $file, ...$args);
30+
}
31+
32+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace CurlInitReturnTypeExtensionTestPhp7;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function (string $unknownString) {
8+
assertType('resource', curl_init());
9+
assertType('resource', curl_init('https://phpstan.org'));
10+
assertType('resource|false', curl_init($unknownString));
11+
assertType('resource', curl_init(null));
12+
assertType('resource', curl_init(''));
13+
assertType('resource|false', curl_init(':'));
14+
assertType('resource|false', curl_init('file://host/text.txt'));
15+
assertType('resource|false', curl_init('FIle://host/text.txt'));
16+
assertType('resource', curl_init('host/text.txt'));
17+
assertType('false', curl_init("\0"));
18+
assertType('false', curl_init("https://phpstan.org\0"));
19+
20+
$url = 'https://phpstan.org';
21+
if (rand(0,1)) $url = null;
22+
assertType('resource', curl_init($url));
23+
24+
$url = 'https://phpstan.org';
25+
if (rand(0,1)) $url = 'https://phpstan.org/try';
26+
assertType('resource', curl_init($url));
27+
28+
$url = 'https://phpstan.org';
29+
if (rand(0,1)) $url = "\0";
30+
assertType('resource|false', curl_init($url));
31+
32+
$url = 'https://phpstan.org';
33+
if (rand(0,1)) $url = "https://phpstan.org\0";
34+
assertType('resource|false', curl_init($url));
35+
36+
$url = 'https://phpstan.org';
37+
if (rand(0,1)) $url = $unknownString;
38+
assertType('resource|false', curl_init($url));
39+
40+
$url = 'https://phpstan.org';
41+
if (rand(0,1)) $url = ':';
42+
assertType('resource|false', curl_init($url));
43+
44+
$url = 'https://phpstan.org';
45+
if (rand(0,1)) $url = 'file://host/text.txt';
46+
assertType('resource|false', curl_init($url));
47+
48+
$url = 'https://phpstan.org';
49+
if (rand(0,1)) $url = 'FIle://host/text.txt';
50+
assertType('resource|false', curl_init($url));
51+
52+
$url = 'https://phpstan.org';
53+
if (rand(0,1)) $url = 'host/text.txt';
54+
assertType('resource', curl_init($url));
55+
56+
$url = 'https://phpstan.org';
57+
if (rand(0,1)) $url = 'file://host/text.txt';
58+
if (rand(0,1)) $url = null;
59+
assertType('resource|false', curl_init($url));
60+
61+
$url = 'https://phpstan.org';
62+
if (rand(0,1)) $url = null;
63+
if (rand(0,1)) $url = $unknownString;
64+
assertType('resource|false', curl_init($url));
65+
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace CurlInitReturnTypeExtensionTestPhp8;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function (string $unknownString) {
8+
assertType('CurlHandle', curl_init());
9+
assertType('CurlHandle', curl_init('https://phpstan.org'));
10+
assertType('CurlHandle|false', curl_init($unknownString));
11+
assertType('CurlHandle', curl_init(null));
12+
assertType('CurlHandle', curl_init(''));
13+
assertType('CurlHandle', curl_init(':'));
14+
assertType('CurlHandle', curl_init('file://host/text.txt'));
15+
assertType('CurlHandle', curl_init('FIle://host/text.txt'));
16+
assertType('CurlHandle', curl_init('host/text.txt'));
17+
assertType('*NEVER*', curl_init("\0"));
18+
assertType('*NEVER*', curl_init("https://phpstan.org\0"));
19+
20+
$url = 'https://phpstan.org';
21+
if (rand(0,1)) $url = null;
22+
assertType('CurlHandle', curl_init($url));
23+
24+
$url = 'https://phpstan.org';
25+
if (rand(0,1)) $url = 'https://phpstan.org/try';
26+
assertType('CurlHandle', curl_init($url));
27+
28+
$url = 'https://phpstan.org';
29+
if (rand(0,1)) $url = "\0";
30+
assertType('CurlHandle', curl_init($url));
31+
32+
$url = 'https://phpstan.org';
33+
if (rand(0,1)) $url = "https://phpstan.org\0";
34+
assertType('CurlHandle', curl_init($url));
35+
36+
$url = 'https://phpstan.org';
37+
if (rand(0,1)) $url = $unknownString;
38+
assertType('CurlHandle|false', curl_init($url));
39+
40+
$url = 'https://phpstan.org';
41+
if (rand(0,1)) $url .= $unknownString;
42+
assertType('CurlHandle|false', curl_init($url));
43+
44+
$url = 'https://phpstan.org';
45+
if (rand(0,1)) $url = ':';
46+
assertType('CurlHandle', curl_init($url));
47+
48+
$url = 'https://phpstan.org';
49+
if (rand(0,1)) $url = 'file://host/text.txt';
50+
assertType('CurlHandle', curl_init($url));
51+
52+
$url = 'https://phpstan.org';
53+
if (rand(0,1)) $url = 'FIle://host/text.txt';
54+
assertType('CurlHandle', curl_init($url));
55+
56+
$url = 'https://phpstan.org';
57+
if (rand(0,1)) $url = 'host/text.txt';
58+
assertType('CurlHandle', curl_init($url));
59+
60+
$url = 'https://phpstan.org';
61+
if (rand(0,1)) $url = 'file://host/text.txt';
62+
if (rand(0,1)) $url = null;
63+
assertType('CurlHandle', curl_init($url));
64+
65+
$url = 'https://phpstan.org';
66+
if (rand(0,1)) $url = null;
67+
if (rand(0,1)) $url = $unknownString;
68+
assertType('CurlHandle|false', curl_init($url));
69+
};

0 commit comments

Comments
 (0)