Skip to content

Commit db7af31

Browse files
authored
Support for numeric string in bc math
1 parent f0d5ed1 commit db7af31

File tree

4 files changed

+81
-73
lines changed

4 files changed

+81
-73
lines changed

.devcontainer/Dockerfile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/shar
1818
RUN echo "memory_limit=768M" > /usr/local/etc/php/php.ini
1919

2020
# Composer v2
21-
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
22-
&& php -r "if (hash_file('sha384', 'composer-setup.php') === '795f976fe0ebd8b75f26a6dd68f78fd3453ce79f32ecb33e7fd087d39bfeb978342fb73ac986cd4f54edd0dc902601dc') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" \
21+
RUN EXPECTED_CHECKSUM="$(wget -q -O - https://composer.github.io/installer.sig)" \
22+
&& php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
23+
&& ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" \
24+
&& if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then >&2 echo 'ERROR: Invalid installer checksum'; rm composer-setup.php; exit 1; fi \
2325
&& php composer-setup.php --version=2.0.0-RC1 \
2426
&& php -r "unlink('composer-setup.php');" \
2527
&& mv composer.phar /usr/local/bin/composer

resources/functionMap.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -419,11 +419,11 @@
419419
'bbcode_parse' => ['string', 'bbcode_container'=>'resource', 'to_parse'=>'string'],
420420
'bbcode_set_arg_parser' => ['bool', 'bbcode_container'=>'resource', 'bbcode_arg_parser'=>'resource'],
421421
'bbcode_set_flags' => ['bool', 'bbcode_container'=>'resource', 'flags'=>'int', 'mode='=>'int'],
422-
'bcadd' => ['string', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'],
423-
'bccomp' => ['int', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'],
424-
'bcdiv' => ['string|null', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'],
425-
'bcmod' => ['string|null', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'],
426-
'bcmul' => ['string', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'],
422+
'bcadd' => ['numeric-string', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'],
423+
'bccomp' => ['int', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'],
424+
'bcdiv' => ['numeric-string|null', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'],
425+
'bcmod' => ['numeric-string|null', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'],
426+
'bcmul' => ['numeric-string', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'],
427427
'bcompiler_load' => ['bool', 'filename'=>'string'],
428428
'bcompiler_load_exe' => ['bool', 'filename'=>'string'],
429429
'bcompiler_parse_class' => ['bool', 'class'=>'string', 'callback'=>'string'],
@@ -437,11 +437,11 @@
437437
'bcompiler_write_functions_from_file' => ['bool', 'filehandle'=>'resource', 'filename'=>'string'],
438438
'bcompiler_write_header' => ['bool', 'filehandle'=>'resource', 'write_ver='=>'string'],
439439
'bcompiler_write_included_filename' => ['bool', 'filehandle'=>'resource', 'filename'=>'string'],
440-
'bcpow' => ['string', 'base'=>'string', 'exponent'=>'string', 'scale='=>'int'],
441-
'bcpowmod' => ['string|null', 'base'=>'string', 'exponent'=>'string', 'modulus'=>'string', 'scale='=>'int'],
440+
'bcpow' => ['numeric-string', 'base'=>'numeric-string', 'exponent'=>'numeric-string', 'scale='=>'int'],
441+
'bcpowmod' => ['numeric-string|null', 'base'=>'numeric-string', 'exponent'=>'numeric-string', 'modulus'=>'numeric-string', 'scale='=>'int'],
442442
'bcscale' => ['int', 'scale='=>'int'],
443-
'bcsqrt' => ['string', 'operand'=>'string', 'scale='=>'int'],
444-
'bcsub' => ['string', 'left_operand'=>'string', 'right_operand'=>'string', 'scale='=>'int'],
443+
'bcsqrt' => ['numeric-string', 'operand'=>'numeric-string', 'scale='=>'int'],
444+
'bcsub' => ['numeric-string', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'],
445445
'bin2hex' => ['string', 'data'=>'string'],
446446
'bind_textdomain_codeset' => ['string', 'domain'=>'string', 'codeset'=>'string'],
447447
'bindec' => ['float|int', 'binary_number'=>'string'],

src/Type/Php/BcMathStringOrNullReturnTypeExtension.php

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
use PhpParser\Node\Expr\UnaryMinus;
77
use PHPStan\Analyser\Scope;
88
use PHPStan\Reflection\FunctionReflection;
9-
use PHPStan\Type\BooleanType;
9+
use PHPStan\Type\Accessory\AccessoryNumericStringType;
1010
use PHPStan\Type\Constant\ConstantBooleanType;
1111
use PHPStan\Type\ConstantScalarType;
1212
use PHPStan\Type\IntegerRangeType;
1313
use PHPStan\Type\IntegerType;
1414
use PHPStan\Type\NullType;
1515
use PHPStan\Type\StringType;
1616
use PHPStan\Type\Type;
17+
use PHPStan\Type\TypeCombinator;
1718
use PHPStan\Type\UnionType;
1819
use function in_array;
1920
use function is_numeric;
@@ -36,10 +37,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3637
return $this->getTypeForBcPowMod($functionCall, $scope);
3738
}
3839

39-
$defaultReturnType = new UnionType([new StringType(), new NullType()]);
40+
$stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType());
41+
42+
$defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]);
4043

4144
if (isset($functionCall->args[1]) === false) {
42-
return $defaultReturnType;
45+
return $stringAndNumericStringType;
4346
}
4447

4548
$secondArgument = $scope->getType($functionCall->args[1]->value);
@@ -51,7 +54,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
5154

5255
if (isset($functionCall->args[2]) === false) {
5356
if ($secondArgument instanceof ConstantScalarType || $secondArgumentIsNumeric) {
54-
return new StringType();
57+
return $stringAndNumericStringType;
5558
}
5659

5760
return $defaultReturnType;
@@ -65,7 +68,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
6568
}
6669

6770
if (($secondArgument instanceof ConstantScalarType || $secondArgumentIsNumeric) && $thirdArgumentIsNumeric) {
68-
return new StringType();
71+
return $stringAndNumericStringType;
6972
}
7073

7174
return $defaultReturnType;
@@ -78,11 +81,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
7881
*
7982
* @param FuncCall $functionCall
8083
* @param Scope $scope
81-
* @return NullType|StringType|UnionType
84+
* @return Type
8285
*/
83-
private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope)
86+
private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type
8487
{
85-
$defaultReturnType = new UnionType([new StringType(), new NullType()]);
88+
$stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType());
89+
$defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]);
8690

8791
if (isset($functionCall->args[0]) === false) {
8892
return $defaultReturnType;
@@ -100,7 +104,7 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope)
100104

101105
if (isset($functionCall->args[1]) === false) {
102106
if ($firstArgumentIsPositive) {
103-
return new StringType();
107+
return $stringAndNumericStringType;
104108
}
105109

106110
return $defaultReturnType;
@@ -115,7 +119,7 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope)
115119
}
116120

117121
if ($firstArgumentIsPositive && $secondArgumentIsValid) {
118-
return new StringType();
122+
return $stringAndNumericStringType;
119123
}
120124

121125
return $defaultReturnType;
@@ -127,12 +131,14 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope)
127131
* > Returns the result as a string, or FALSE if modulus is 0 or exponent is negative.
128132
* @param FuncCall $functionCall
129133
* @param Scope $scope
130-
* @return BooleanType|StringType|UnionType
134+
* @return Type
131135
*/
132-
private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope)
136+
private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type
133137
{
138+
$stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType());
139+
134140
if (isset($functionCall->args[1]) === false) {
135-
return new UnionType([new StringType(), new ConstantBooleanType(false)]);
141+
return new UnionType([$stringAndNumericStringType, new ConstantBooleanType(false)]);
136142
}
137143

138144
$exponent = $scope->getType($functionCall->args[1]->value);
@@ -156,11 +162,11 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope)
156162
}
157163

158164
if ($modulus instanceof ConstantScalarType) {
159-
return new StringType();
165+
return $stringAndNumericStringType;
160166
}
161167
}
162168

163-
return new UnionType([new StringType(), new ConstantBooleanType(false)]);
169+
return new UnionType([$stringAndNumericStringType, new ConstantBooleanType(false)]);
164170
}
165171

166172
/**

tests/PHPStan/Analyser/data/bcmath-dynamic-return.php

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -17,43 +17,43 @@
1717

1818

1919
// bcdiv ( string $dividend , string $divisor [, int $scale = 0 ] ) : string
20-
// Returns the result of the division as a string, or NULL if divisor is 0.
20+
// Returns the result of the division as a numeric-string, or NULL if divisor is 0.
2121
\PHPStan\Analyser\assertType('null', bcdiv('10', '0')); // Warning: Division by zero
2222
\PHPStan\Analyser\assertType('null', bcdiv('10', '0.0')); // Warning: Division by zero
2323
\PHPStan\Analyser\assertType('null', bcdiv('10', 0.0)); // Warning: Division by zero
24-
\PHPStan\Analyser\assertType('string', bcdiv('10', '1'));
25-
\PHPStan\Analyser\assertType('string', bcdiv('10', '-1'));
26-
\PHPStan\Analyser\assertType('string', bcdiv('10', '2', 0));
27-
\PHPStan\Analyser\assertType('string', bcdiv('10', '2', 1));
28-
\PHPStan\Analyser\assertType('string', bcdiv('10', $iNeg));
29-
\PHPStan\Analyser\assertType('string', bcdiv('10', $iPos));
30-
\PHPStan\Analyser\assertType('string', bcdiv($iPos, $iPos));
31-
\PHPStan\Analyser\assertType('string|null', bcdiv('10', $mixed));
32-
\PHPStan\Analyser\assertType('string', bcdiv('10', $iPos, $iPos));
33-
\PHPStan\Analyser\assertType('string', bcdiv('10', $iUnknown));
24+
\PHPStan\Analyser\assertType('string&numeric', bcdiv('10', '1'));
25+
\PHPStan\Analyser\assertType('string&numeric', bcdiv('10', '-1'));
26+
\PHPStan\Analyser\assertType('string&numeric', bcdiv('10', '2', 0));
27+
\PHPStan\Analyser\assertType('string&numeric', bcdiv('10', '2', 1));
28+
\PHPStan\Analyser\assertType('string&numeric', bcdiv('10', $iNeg));
29+
\PHPStan\Analyser\assertType('string&numeric', bcdiv('10', $iPos));
30+
\PHPStan\Analyser\assertType('string&numeric', bcdiv($iPos, $iPos));
31+
\PHPStan\Analyser\assertType('(string&numeric)|null', bcdiv('10', $mixed));
32+
\PHPStan\Analyser\assertType('string&numeric', bcdiv('10', $iPos, $iPos));
33+
\PHPStan\Analyser\assertType('string&numeric', bcdiv('10', $iUnknown));
3434
\PHPStan\Analyser\assertType('null', bcdiv('10', $iPos, $nonNumeric)); // Warning: expects parameter 3 to be int, string given in
3535
\PHPStan\Analyser\assertType('null', bcdiv('10', $nonNumeric)); // Warning: bcmath function argument is not well-formed
3636

3737
// bcmod ( string $dividend , string $divisor [, int $scale = 0 ] ) : string
38-
// Returns the modulus as a string, or NULL if divisor is 0.
38+
// Returns the modulus as a numeric-string, or NULL if divisor is 0.
3939
\PHPStan\Analyser\assertType('null', bcmod('10', '0'));
4040
\PHPStan\Analyser\assertType('null', bcmod($iPos, '0')); // Warning: Division by zero
4141
\PHPStan\Analyser\assertType('null', bcmod('10', $nonNumeric));
42-
\PHPStan\Analyser\assertType('string', bcmod('10', '1'));
43-
\PHPStan\Analyser\assertType('string', bcmod('10', '2', 0));
44-
\PHPStan\Analyser\assertType('string', bcmod('5.7', '1.3', 1));
45-
\PHPStan\Analyser\assertType('string', bcmod('10', 2.2));
46-
\PHPStan\Analyser\assertType('string', bcmod('10', $iUnknown));
47-
\PHPStan\Analyser\assertType('string', bcmod('10', '-1'));
48-
\PHPStan\Analyser\assertType('string', bcmod($iPos, '-1'));
49-
\PHPStan\Analyser\assertType('string', bcmod('10', $iNeg));
50-
\PHPStan\Analyser\assertType('string', bcmod('10', $iPos));
51-
\PHPStan\Analyser\assertType('string', bcmod('10', -$iNeg));
52-
\PHPStan\Analyser\assertType('string', bcmod('10', -$iPos));
53-
\PHPStan\Analyser\assertType('string|null', bcmod('10', $mixed));
42+
\PHPStan\Analyser\assertType('string&numeric', bcmod('10', '1'));
43+
\PHPStan\Analyser\assertType('string&numeric', bcmod('10', '2', 0));
44+
\PHPStan\Analyser\assertType('string&numeric', bcmod('5.7', '1.3', 1));
45+
\PHPStan\Analyser\assertType('string&numeric', bcmod('10', 2.2));
46+
\PHPStan\Analyser\assertType('string&numeric', bcmod('10', $iUnknown));
47+
\PHPStan\Analyser\assertType('string&numeric', bcmod('10', '-1'));
48+
\PHPStan\Analyser\assertType('string&numeric', bcmod($iPos, '-1'));
49+
\PHPStan\Analyser\assertType('string&numeric', bcmod('10', $iNeg));
50+
\PHPStan\Analyser\assertType('string&numeric', bcmod('10', $iPos));
51+
\PHPStan\Analyser\assertType('string&numeric', bcmod('10', -$iNeg));
52+
\PHPStan\Analyser\assertType('string&numeric', bcmod('10', -$iPos));
53+
\PHPStan\Analyser\assertType('(string&numeric)|null', bcmod('10', $mixed));
5454

5555
// bcpowmod ( string $base , string $exponent , string $modulus [, int $scale = 0 ] ) : string
56-
// Returns the result as a string, or FALSE if modulus is 0 or exponent is negative.
56+
// Returns the result as a numeric-string, or FALSE if modulus is 0 or exponent is negative.
5757
\PHPStan\Analyser\assertType('false', bcpowmod('10', '-2', '0')); // exponent negative, and modulus is 0
5858
\PHPStan\Analyser\assertType('false', bcpowmod('10', '-2', '1')); // exponent negative
5959
\PHPStan\Analyser\assertType('false', bcpowmod('10', '2', $nonNumeric)); // Warning: bcmath function argument is not well-formed
@@ -66,32 +66,32 @@
6666
\PHPStan\Analyser\assertType('false', bcpowmod('10', '2', '0')); // modulus is 0
6767
\PHPStan\Analyser\assertType('false', bcpowmod('10', 2.3, '0')); // modulus is 0
6868
\PHPStan\Analyser\assertType('false', bcpowmod('10', '0', '0')); // modulus is 0
69-
\PHPStan\Analyser\assertType('string', bcpowmod('10', '0', '-2'));
70-
\PHPStan\Analyser\assertType('string', bcpowmod('10', '2', '2'));
71-
\PHPStan\Analyser\assertType('string', bcpowmod('10', $iUnknown, '2'));
72-
\PHPStan\Analyser\assertType('string', bcpowmod($iPos, '2', '2'));
73-
\PHPStan\Analyser\assertType('string|false', bcpowmod('10', $mixed, $mixed));
74-
\PHPStan\Analyser\assertType('string', bcpowmod('10', '2', '2'));
75-
\PHPStan\Analyser\assertType('string', bcpowmod('10', -$iNeg, '2'));
76-
\PHPStan\Analyser\assertType('string', bcpowmod('10', $nonNumeric, '2')); // Warning: bcmath function argument is not well-formed
77-
\PHPStan\Analyser\assertType('string|false', bcpowmod('10', $iUnknown, $iUnknown));
69+
\PHPStan\Analyser\assertType('string&numeric', bcpowmod('10', '0', '-2'));
70+
\PHPStan\Analyser\assertType('string&numeric', bcpowmod('10', '2', '2'));
71+
\PHPStan\Analyser\assertType('string&numeric', bcpowmod('10', $iUnknown, '2'));
72+
\PHPStan\Analyser\assertType('string&numeric', bcpowmod($iPos, '2', '2'));
73+
\PHPStan\Analyser\assertType('(string&numeric)|false', bcpowmod('10', $mixed, $mixed));
74+
\PHPStan\Analyser\assertType('string&numeric', bcpowmod('10', '2', '2'));
75+
\PHPStan\Analyser\assertType('string&numeric', bcpowmod('10', -$iNeg, '2'));
76+
\PHPStan\Analyser\assertType('string&numeric', bcpowmod('10', $nonNumeric, '2')); // Warning: bcmath function argument is not well-formed
77+
\PHPStan\Analyser\assertType('(string&numeric)|false', bcpowmod('10', $iUnknown, $iUnknown));
7878

7979
// bcsqrt ( string $operand [, int $scale = 0 ] ) : string
80-
// Returns the square root as a string, or NULL if operand is negative.
81-
\PHPStan\Analyser\assertType('string', bcsqrt('10', $iNeg));
82-
\PHPStan\Analyser\assertType('string', bcsqrt('10', 1));
83-
\PHPStan\Analyser\assertType('string', bcsqrt('0.00', 1));
84-
\PHPStan\Analyser\assertType('string', bcsqrt(0.0, 1));
85-
\PHPStan\Analyser\assertType('string', bcsqrt('0', 1));
86-
\PHPStan\Analyser\assertType('string|null', bcsqrt($iUnknown, $iUnknown));
87-
\PHPStan\Analyser\assertType('string', bcsqrt('10', $iPos));
80+
// Returns the square root as a numeric-string, or NULL if operand is negative.
81+
\PHPStan\Analyser\assertType('string&numeric', bcsqrt('10', $iNeg));
82+
\PHPStan\Analyser\assertType('string&numeric', bcsqrt('10', 1));
83+
\PHPStan\Analyser\assertType('string&numeric', bcsqrt('0.00', 1));
84+
\PHPStan\Analyser\assertType('string&numeric', bcsqrt(0.0, 1));
85+
\PHPStan\Analyser\assertType('string&numeric', bcsqrt('0', 1));
86+
\PHPStan\Analyser\assertType('(string&numeric)|null', bcsqrt($iUnknown, $iUnknown));
87+
\PHPStan\Analyser\assertType('string&numeric', bcsqrt('10', $iPos));
8888
\PHPStan\Analyser\assertType('null', bcsqrt('-10', 0)); // Warning: Square root of negative number
8989
\PHPStan\Analyser\assertType('null', bcsqrt($iNeg, 0));
9090
\PHPStan\Analyser\assertType('null', bcsqrt('10', $nonNumeric)); // Warning: Second argument must be ?int (Fatal in PHP8)
91-
\PHPStan\Analyser\assertType('string', bcsqrt('10'));
92-
\PHPStan\Analyser\assertType('string|null', bcsqrt($iUnknown));
91+
\PHPStan\Analyser\assertType('string&numeric', bcsqrt('10'));
92+
\PHPStan\Analyser\assertType('(string&numeric)|null', bcsqrt($iUnknown));
9393
\PHPStan\Analyser\assertType('null', bcsqrt('-10')); // Warning: Square root of negative number
9494

95-
\PHPStan\Analyser\assertType('string|null', bcsqrt($nonNumeric, -1)); // Warning: bcmath function argument is not well-formed
96-
\PHPStan\Analyser\assertType('string|null', bcsqrt('10', $mixed));
97-
\PHPStan\Analyser\assertType('string', bcsqrt($iPos));
95+
\PHPStan\Analyser\assertType('(string&numeric)|null', bcsqrt($nonNumeric, -1)); // Warning: bcmath function argument is not well-formed
96+
\PHPStan\Analyser\assertType('(string&numeric)|null', bcsqrt('10', $mixed));
97+
\PHPStan\Analyser\assertType('string&numeric', bcsqrt($iPos));

0 commit comments

Comments
 (0)