Skip to content

Commit 06328f0

Browse files
W0rmaondrejmirtes
authored andcommitted
Fix return types for bcdiv(), bcmod(), bcpowmod() and bcsqrt() in PHP 8.0 and higher
1 parent 3e9b629 commit 06328f0

File tree

4 files changed

+201
-8
lines changed

4 files changed

+201
-8
lines changed

src/Type/Php/BcMathStringOrNullReturnTypeExtension.php

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
use PhpParser\Node\Expr\FuncCall;
66
use PhpParser\Node\Expr\UnaryMinus;
77
use PHPStan\Analyser\Scope;
8+
use PHPStan\Php\PhpVersion;
89
use PHPStan\Reflection\FunctionReflection;
910
use PHPStan\Type\Accessory\AccessoryNumericStringType;
1011
use PHPStan\Type\Constant\ConstantBooleanType;
1112
use PHPStan\Type\ConstantScalarType;
1213
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1314
use PHPStan\Type\IntegerRangeType;
1415
use PHPStan\Type\IntegerType;
16+
use PHPStan\Type\NeverType;
1517
use PHPStan\Type\NullType;
1618
use PHPStan\Type\StringType;
1719
use PHPStan\Type\Type;
@@ -23,6 +25,10 @@
2325
class BcMathStringOrNullReturnTypeExtension implements DynamicFunctionReturnTypeExtension
2426
{
2527

28+
public function __construct(private PhpVersion $phpVersion)
29+
{
30+
}
31+
2632
public function isFunctionSupported(FunctionReflection $functionReflection): bool
2733
{
2834
return in_array($functionReflection->getName(), ['bcdiv', 'bcmod', 'bcpowmod', 'bcsqrt'], true);
@@ -40,16 +46,28 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
4046

4147
$stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType());
4248

43-
$defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]);
44-
4549
if (isset($functionCall->getArgs()[1]) === false) {
46-
return $stringAndNumericStringType;
50+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
51+
return new NeverType();
52+
}
53+
54+
return new NullType();
55+
}
56+
57+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
58+
$defaultReturnType = $stringAndNumericStringType;
59+
} else {
60+
$defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]);
4761
}
4862

4963
$secondArgument = $scope->getType($functionCall->getArgs()[1]->value);
5064
$secondArgumentIsNumeric = ($secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue())) || $secondArgument instanceof IntegerType;
5165

5266
if ($secondArgument instanceof ConstantScalarType && ($this->isZero($secondArgument->getValue()) || !$secondArgumentIsNumeric)) {
67+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
68+
return new NeverType();
69+
}
70+
5371
return new NullType();
5472
}
5573

@@ -62,12 +80,30 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
6280
}
6381

6482
$thirdArgument = $scope->getType($functionCall->getArgs()[2]->value);
65-
$thirdArgumentIsNumeric = ($thirdArgument instanceof ConstantScalarType && is_numeric($thirdArgument->getValue())) || $thirdArgument instanceof IntegerType;
83+
$thirdArgumentIsNumeric = false;
84+
$thirdArgumentIsNegative = false;
85+
if ($thirdArgument instanceof ConstantScalarType && is_numeric($thirdArgument->getValue())) {
86+
$thirdArgumentIsNumeric = true;
87+
$thirdArgumentIsNegative = ($thirdArgument->getValue() < 0);
88+
} elseif ((new IntegerType())->isSuperTypeOf($thirdArgument)->yes()) {
89+
$thirdArgumentIsNumeric = true;
90+
if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($thirdArgument)->yes()) {
91+
$thirdArgumentIsNegative = true;
92+
}
93+
}
6694

6795
if ($thirdArgument instanceof ConstantScalarType && !is_numeric($thirdArgument->getValue())) {
96+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
97+
return new NeverType();
98+
}
99+
68100
return new NullType();
69101
}
70102

103+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions() && $thirdArgumentIsNegative) {
104+
return new NeverType();
105+
}
106+
71107
if (($secondArgument instanceof ConstantScalarType || $secondArgumentIsNumeric) && $thirdArgumentIsNumeric) {
72108
return $stringAndNumericStringType;
73109
}
@@ -84,9 +120,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
84120
private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type
85121
{
86122
$stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType());
87-
$defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]);
123+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
124+
$defaultReturnType = $stringAndNumericStringType;
125+
} else {
126+
$defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]);
127+
}
88128

89129
if (isset($functionCall->getArgs()[0]) === false) {
130+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
131+
return new NeverType();
132+
}
133+
90134
return $defaultReturnType;
91135
}
92136

@@ -95,8 +139,11 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type
95139
$firstArgumentIsPositive = $firstArgument instanceof ConstantScalarType && is_numeric($firstArgument->getValue()) && $firstArgument->getValue() >= 0;
96140
$firstArgumentIsNegative = $firstArgument instanceof ConstantScalarType && is_numeric($firstArgument->getValue()) && $firstArgument->getValue() < 0;
97141

98-
if ($firstArgument instanceof UnaryMinus ||
99-
($firstArgumentIsNegative)) {
142+
if ($firstArgument instanceof UnaryMinus || $firstArgumentIsNegative) {
143+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
144+
return new NeverType();
145+
}
146+
100147
return new NullType();
101148
}
102149

@@ -111,11 +158,22 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type
111158
$secondArgument = $scope->getType($functionCall->getArgs()[1]->value);
112159
$secondArgumentIsValid = $secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue()) && !$this->isZero($secondArgument->getValue());
113160
$secondArgumentIsNonNumeric = $secondArgument instanceof ConstantScalarType && !is_numeric($secondArgument->getValue());
161+
$secondArgumentIsNegative = $secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue()) && $secondArgument->getValue() < 0;
114162

115163
if ($secondArgumentIsNonNumeric) {
164+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
165+
return new NeverType();
166+
}
167+
116168
return new NullType();
117169
}
118170

171+
if ($secondArgument instanceof UnaryMinus || $secondArgumentIsNegative) {
172+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
173+
return new NeverType();
174+
}
175+
}
176+
119177
if ($firstArgumentIsPositive && $secondArgumentIsValid) {
120178
return $stringAndNumericStringType;
121179
}
@@ -130,20 +188,40 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type
130188
*/
131189
private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type
132190
{
191+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions() && isset($functionCall->getArgs()[0]) === false) {
192+
return new NeverType();
193+
}
194+
133195
$stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType());
134196

135197
if (isset($functionCall->getArgs()[1]) === false) {
198+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
199+
return new NeverType();
200+
}
201+
136202
return new UnionType([$stringAndNumericStringType, new ConstantBooleanType(false)]);
137203
}
138204

139205
$exponent = $scope->getType($functionCall->getArgs()[1]->value);
206+
207+
// Expontent is non numeric
208+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()
209+
&& $exponent instanceof ConstantScalarType && !is_numeric($exponent->getValue())
210+
) {
211+
return new NeverType();
212+
}
213+
140214
$exponentIsNegative = IntegerRangeType::fromInterval(null, 0)->isSuperTypeOf($exponent)->yes();
141215

142216
if ($exponent instanceof ConstantScalarType) {
143217
$exponentIsNegative = is_numeric($exponent->getValue()) && $exponent->getValue() < 0;
144218
}
145219

146220
if ($exponentIsNegative) {
221+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
222+
return new NeverType();
223+
}
224+
147225
return new ConstantBooleanType(false);
148226
}
149227

@@ -153,12 +231,24 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type
153231
$modulusIsNonNumeric = $modulus instanceof ConstantScalarType && !is_numeric($modulus->getValue());
154232

155233
if ($modulusIsZero || $modulusIsNonNumeric) {
234+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
235+
return new NeverType();
236+
}
237+
156238
return new ConstantBooleanType(false);
157239
}
158240

159241
if ($modulus instanceof ConstantScalarType) {
160242
return $stringAndNumericStringType;
161243
}
244+
} else {
245+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
246+
return new NeverType();
247+
}
248+
}
249+
250+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
251+
return $stringAndNumericStringType;
162252
}
163253

164254
return new UnionType([$stringAndNumericStringType, new ConstantBooleanType(false)]);

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,13 @@ public function dataFileAsserts(): iterable
148148
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-2550.php');
149149
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2899.php');
150150
yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_split.php');
151-
yield from $this->gatherAssertTypes(__DIR__ . '/data/bcmath-dynamic-return.php');
151+
152+
if (PHP_VERSION_ID < 80000) {
153+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bcmath-dynamic-return-php7.php');
154+
} else {
155+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bcmath-dynamic-return-php8.php');
156+
}
157+
152158
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3875.php');
153159
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2611.php');
154160
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3548.php');
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
// Verification for constant types: https://3v4l.org/96GSj
4+
5+
/** @var mixed $mixed */
6+
$mixed = getMixed();
7+
8+
/** @var int $iUnknown */
9+
$iUnknown = getInt();
10+
11+
/** @var string $string */
12+
$string = getString();
13+
14+
$iNeg = -5;
15+
$iPos = 5;
16+
$nonNumeric = 'foo';
17+
18+
19+
// bcdiv ( string $dividend , string $divisor [, ?int $scale = null ] ) : string
20+
// Returns the result of the division as a numeric-string.
21+
\PHPStan\Testing\assertType('*NEVER*', bcdiv('10', '0')); // DivisionByZeroError
22+
\PHPStan\Testing\assertType('*NEVER*', bcdiv('10', '0.0')); // DivisionByZeroError
23+
\PHPStan\Testing\assertType('*NEVER*', bcdiv('10', 0.0)); // DivisionByZeroError
24+
\PHPStan\Testing\assertType('numeric-string', bcdiv('10', '1'));
25+
\PHPStan\Testing\assertType('numeric-string', bcdiv('10', '-1'));
26+
\PHPStan\Testing\assertType('numeric-string', bcdiv('10', '2', 0));
27+
\PHPStan\Testing\assertType('numeric-string', bcdiv('10', '2', 1));
28+
\PHPStan\Testing\assertType('numeric-string', bcdiv('10', $iNeg));
29+
\PHPStan\Testing\assertType('numeric-string', bcdiv('10', $iPos));
30+
\PHPStan\Testing\assertType('numeric-string', bcdiv($iPos, $iPos));
31+
\PHPStan\Testing\assertType('numeric-string', bcdiv('10', $mixed));
32+
\PHPStan\Testing\assertType('numeric-string', bcdiv('10', $iPos, $iPos));
33+
\PHPStan\Testing\assertType('numeric-string', bcdiv('10', $iUnknown));
34+
\PHPStan\Testing\assertType('*NEVER*', bcdiv('10', $iPos, $nonNumeric)); // ValueError argument 3
35+
\PHPStan\Testing\assertType('*NEVER*', bcdiv('10', $nonNumeric)); // ValueError argument 2
36+
37+
// bcmod ( string $dividend , string $divisor [, ?int $scale = null ] ) : string
38+
// Returns the modulus as a numeric-string.
39+
\PHPStan\Testing\assertType('*NEVER*', bcmod('10', '0')); // DivisionByZeroError
40+
\PHPStan\Testing\assertType('*NEVER*', bcmod($iPos, '0')); // DivisionByZeroError
41+
\PHPStan\Testing\assertType('*NEVER*', bcmod('10', $nonNumeric)); // ValueError argument 2
42+
\PHPStan\Testing\assertType('numeric-string', bcmod('10', '1'));
43+
\PHPStan\Testing\assertType('numeric-string', bcmod('10', '2', 0));
44+
\PHPStan\Testing\assertType('numeric-string', bcmod('5.7', '1.3', 1));
45+
\PHPStan\Testing\assertType('numeric-string', bcmod('10', 2.2));
46+
\PHPStan\Testing\assertType('numeric-string', bcmod('10', $iUnknown));
47+
\PHPStan\Testing\assertType('numeric-string', bcmod('10', '-1'));
48+
\PHPStan\Testing\assertType('numeric-string', bcmod($iPos, '-1'));
49+
\PHPStan\Testing\assertType('numeric-string', bcmod('10', $iNeg));
50+
\PHPStan\Testing\assertType('numeric-string', bcmod('10', $iPos));
51+
\PHPStan\Testing\assertType('numeric-string', bcmod('10', -$iNeg));
52+
\PHPStan\Testing\assertType('numeric-string', bcmod('10', -$iPos));
53+
\PHPStan\Testing\assertType('numeric-string', bcmod('10', $mixed));
54+
55+
// bcpowmod ( string $base , string $exponent , string $modulus [, ?int $scale = null ] ) : string
56+
// Returns the result as a numeric-string, or FALSE if modulus is 0 or exponent is negative.
57+
\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '-2', '0')); // ValueError argument 2
58+
\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '-2', '1')); // ValueError argument 2
59+
\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '2', $nonNumeric)); // ValueError argument 3
60+
\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '-2', '-1')); // ValueError argument 2
61+
\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '-2', -1.3)); // ValueError argument 2
62+
\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', -$iPos, '-1')); // ValueError argument 2
63+
\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', -$iPos, '1')); // ValueError argument 2
64+
\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', $nonNumeric, $nonNumeric)); // ValueError argument 2
65+
\PHPStan\Testing\assertType('*NEVER*', bcpowmod($iPos, $nonNumeric, $nonNumeric));
66+
\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '2', '0')); // modulus is 0
67+
\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', 2.3, '0')); // modulus is 0
68+
\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', '0', '0')); // modulus is 0
69+
\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', '0', '-2'));
70+
\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', '2', '2'));
71+
\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', $iUnknown, '2'));
72+
\PHPStan\Testing\assertType('numeric-string', bcpowmod($iPos, '2', '2'));
73+
\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', $mixed, $mixed));
74+
\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', '2', '2'));
75+
\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', -$iNeg, '2'));
76+
\PHPStan\Testing\assertType('*NEVER*', bcpowmod('10', $nonNumeric, '2')); // ValueError argument 2
77+
\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', $iUnknown, $iUnknown));
78+
79+
// bcsqrt ( string $operand [, ?int $scale = null ] ) : string
80+
// Returns the square root as a numeric-string.
81+
\PHPStan\Testing\assertType('*NEVER*', bcsqrt('10', $iNeg)); // ValueError argument 2
82+
\PHPStan\Testing\assertType('numeric-string', bcsqrt('10', 1));
83+
\PHPStan\Testing\assertType('numeric-string', bcsqrt('0.00', 1));
84+
\PHPStan\Testing\assertType('numeric-string', bcsqrt(0.0, 1));
85+
\PHPStan\Testing\assertType('numeric-string', bcsqrt('0', 1));
86+
\PHPStan\Testing\assertType('numeric-string', bcsqrt($iUnknown, $iUnknown));
87+
\PHPStan\Testing\assertType('numeric-string', bcsqrt('10', $iPos));
88+
\PHPStan\Testing\assertType('*NEVER*', bcsqrt('-10', 0)); // ValueError argument 1
89+
\PHPStan\Testing\assertType('*NEVER*', bcsqrt($iNeg, null)); // ValueError argument 1
90+
\PHPStan\Testing\assertType('*NEVER*', bcsqrt('10', $nonNumeric)); // ValueError argument 2
91+
\PHPStan\Testing\assertType('numeric-string', bcsqrt('10'));
92+
\PHPStan\Testing\assertType('numeric-string', bcsqrt($iUnknown));
93+
\PHPStan\Testing\assertType('*NEVER*', bcsqrt('-10')); // ValueError argument 1
94+
95+
\PHPStan\Testing\assertType('*NEVER*', bcsqrt($nonNumeric, -1)); // ValueError argument 1
96+
\PHPStan\Testing\assertType('numeric-string', bcsqrt('10', $mixed));
97+
\PHPStan\Testing\assertType('numeric-string', bcsqrt($iPos));

0 commit comments

Comments
 (0)