Skip to content

Commit 62b95c6

Browse files
Fix version_compare return type
1 parent 6b33ea1 commit 62b95c6

File tree

5 files changed

+107
-5
lines changed

5 files changed

+107
-5
lines changed

resources/functionMap.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12725,7 +12725,7 @@
1272512725
'VarnishStat::__construct' => ['void', 'args='=>'array'],
1272612726
'VarnishStat::getSnapshot' => ['array'],
1272712727
'version_compare' => ['int', 'version1'=>'string', 'version2'=>'string'],
12728-
'version_compare\'1' => ['bool', 'version1'=>'string', 'version2'=>'string', 'operator'=>'string|null'],
12728+
'version_compare\'1' => ['__benevolent<bool|null>', 'version1'=>'string', 'version2'=>'string', 'operator'=>'string|null'],
1272912729
'vfprintf' => ['int', 'stream'=>'resource', 'format'=>'string', 'args'=>'array<__stringAndStringable|int|float|null|bool>'],
1273012730
'virtual' => ['bool', 'uri'=>'string'],
1273112731
'Volatile::__construct' => ['void'],

src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,50 @@
1010
use PHPStan\Php\ComposerPhpVersionFactory;
1111
use PHPStan\Php\PhpVersion;
1212
use PHPStan\Reflection\FunctionReflection;
13+
use PHPStan\Type\BenevolentUnionType;
1314
use PHPStan\Type\BooleanType;
1415
use PHPStan\Type\Constant\ConstantBooleanType;
1516
use PHPStan\Type\Constant\ConstantIntegerType;
1617
use PHPStan\Type\Constant\ConstantStringType;
1718
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
19+
use PHPStan\Type\NullType;
1820
use PHPStan\Type\Type;
1921
use PHPStan\Type\TypeCombinator;
2022
use function array_filter;
2123
use function count;
24+
use function in_array;
2225
use function is_array;
2326
use function version_compare;
2427

2528
#[AutowiredService]
2629
final class VersionCompareFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
2730
{
2831

32+
private const VALID_OPERATORS = [
33+
'<',
34+
'lt',
35+
'<=',
36+
'le',
37+
'>',
38+
'gt',
39+
'>=',
40+
'ge',
41+
'==',
42+
'=',
43+
'eq',
44+
'!=',
45+
'<>',
46+
'ne',
47+
];
48+
2949
/**
3050
* @param int|array{min: int, max: int}|null $configPhpVersion
3151
*/
3252
public function __construct(
3353
#[AutowiredParameter(ref: '%phpVersion%')]
3454
private int|array|null $configPhpVersion,
3555
private ComposerPhpVersionFactory $composerPhpVersionFactory,
56+
private PhpVersion $phpVersion,
3657
)
3758
{
3859
}
@@ -63,7 +84,9 @@ public function getTypeFromFunctionCall(
6384
if (isset($args[2])) {
6485
$operatorStrings = $scope->getType($args[2]->value)->getConstantStrings();
6586
$counts[] = count($operatorStrings);
66-
$returnType = new BooleanType();
87+
$returnType = $this->phpVersion->throwsValueErrorForInternalFunctions()
88+
? new BooleanType()
89+
: new BenevolentUnionType([new BooleanType(), new NullType()]);
6790
} else {
6891
$returnType = TypeCombinator::union(
6992
new ConstantIntegerType(-1),
@@ -81,11 +104,21 @@ public function getTypeFromFunctionCall(
81104
}
82105

83106
$types = [];
107+
$canBeNull = false;
84108
foreach ($version1Strings as $version1String) {
85109
foreach ($version2Strings as $version2String) {
86110
if (isset($operatorStrings)) {
87111
foreach ($operatorStrings as $operatorString) {
88-
$value = version_compare($version1String->getValue(), $version2String->getValue(), $operatorString->getValue());
112+
$operatorValue = $operatorString->getValue();
113+
if (!in_array($operatorValue, self::VALID_OPERATORS, true)) {
114+
if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) {
115+
$canBeNull = true;
116+
}
117+
118+
continue;
119+
}
120+
121+
$value = version_compare($version1String->getValue(), $version2String->getValue(), $operatorValue);
89122
$types[$value] = new ConstantBooleanType($value);
90123
}
91124
} else {
@@ -94,6 +127,11 @@ public function getTypeFromFunctionCall(
94127
}
95128
}
96129
}
130+
131+
if ($canBeNull) {
132+
$types[] = new NullType();
133+
}
134+
97135
return TypeCombinator::union(...$types);
98136
}
99137

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5237,11 +5237,11 @@ public static function dataFunctions(): array
52375237
'$versionCompare6',
52385238
],
52395239
[
5240-
'bool',
5240+
PHP_VERSION_ID < 80000 ? '(bool|null)' : 'bool',
52415241
'$versionCompare7',
52425242
],
52435243
[
5244-
'bool',
5244+
PHP_VERSION_ID < 80000 ? '(bool|null)' : 'bool',
52455245
'$versionCompare8',
52465246
],
52475247
[
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php // lint < 8.0
2+
3+
declare(strict_types=1);
4+
5+
namespace VersionComparePHP7;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
class Foo
10+
{
11+
/**
12+
* @param string $string
13+
* @param '<'|'>' $unionValid
14+
* @param '<'|'a' $unionBoth
15+
* @param 'a'|'b' $unionInvalid
16+
*/
17+
public function fgetss(
18+
string $string,
19+
string $unionValid,
20+
string $unionBoth,
21+
string $unionInvalid,
22+
) : void
23+
{
24+
assertType('(bool|null)', \version_compare($string, $string, $string));
25+
26+
assertType('false', \version_compare('Foo','Bar','<'));
27+
assertType('(bool|null)', \version_compare('Foo','Bar', $string));
28+
assertType('false', \version_compare('Foo','Bar', $unionValid));
29+
assertType('false|null', \version_compare('Foo','Bar', $unionBoth));
30+
assertType('null', \version_compare('Foo','Bar', $unionInvalid));
31+
}
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types=1);
4+
5+
namespace VersionComparePHP8;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
class Foo
10+
{
11+
/**
12+
* @param string $string
13+
* @param '<'|'>' $unionValid
14+
* @param '<'|'a' $unionBoth
15+
* @param 'a'|'b' $unionInvalid
16+
*/
17+
public function fgetss(
18+
string $string,
19+
string $unionValid,
20+
string $unionBoth,
21+
string $unionInvalid,
22+
) : void
23+
{
24+
assertType('bool', \version_compare($string, $string, $string));
25+
26+
assertType('false', \version_compare('Foo','Bar','<'));
27+
assertType('bool', \version_compare('Foo','Bar', $string));
28+
assertType('false', \version_compare('Foo','Bar', $unionValid));
29+
assertType('false', \version_compare('Foo','Bar', $unionBoth));
30+
assertType('*NEVER*', \version_compare('Foo','Bar', $unionInvalid));
31+
}
32+
}

0 commit comments

Comments
 (0)