Skip to content

Commit 01c4cb5

Browse files
committed
Add BcMathNumberOperatorTypeSpecifyingExtension.php
1 parent 8ceb905 commit 01c4cb5

File tree

3 files changed

+232
-0
lines changed

3 files changed

+232
-0
lines changed

conf/config.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,10 @@ services:
10451045
-
10461046
class: PHPStan\Rules\TooWideTypehints\TooWideParameterOutTypeCheck
10471047

1048+
-
1049+
class: PHPStan\Type\BcMathNumberOperatorTypeSpecifyingExtension
1050+
tags:
1051+
- phpstan.broker.operatorTypeSpecifyingExtension
10481052
-
10491053
class: PHPStan\Type\FileTypeMapper
10501054
arguments:
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use BcMath\Number;
6+
use PHPStan\Node\Expr\TypeExpr;
7+
use PHPStan\Php\PhpVersion;
8+
use PHPStan\Reflection\InitializerExprContext;
9+
use PHPStan\Reflection\InitializerExprTypeResolver;
10+
use PHPStan\Type\Constant\ConstantArrayType;
11+
use PHPStan\Type\TypeCombinator;
12+
use PHPStan\Type\TypeUtils;
13+
use PhpParser\Node\Expr;
14+
use PhpParser\Node\Expr\BinaryOp;
15+
use function count;
16+
use function in_array;
17+
18+
/**
19+
* @see https://wiki.php.net/rfc/support_object_type_in_bcmath
20+
*/
21+
final class BcMathNumberOperatorTypeSpecifyingExtension implements OperatorTypeSpecifyingExtension
22+
{
23+
24+
private const OPERATORS = [
25+
'-' => BinaryOp\Minus::class,
26+
'+' => BinaryOp\Plus::class,
27+
'*' => BinaryOp\Mul::class,
28+
'/' => BinaryOp\Div::class,
29+
'**' => BinaryOp\Pow::class,
30+
'%' => BinaryOp\Mod::class,
31+
];
32+
33+
public function __construct(
34+
private PhpVersion $phpVersion,
35+
private InitializerExprTypeResolver $InitializerExprTypeResolver,
36+
) {
37+
}
38+
39+
public function isOperatorSupported(string $operatorSigil, Type $leftSide, Type $rightSide): bool
40+
{
41+
if (!$this->phpVersion->supportsBcMathNumberOperatorOverloading()) {
42+
return false;
43+
}
44+
45+
return in_array($operatorSigil, ['-', '+', '*', '/', '**', '%'], true)
46+
&& (
47+
$leftSide->isSuperTypeOf(new ObjectType('BcMath\Number'))->yes()
48+
|| $rightSide->isSuperTypeOf(new ObjectType('BcMath\Number'))->yes()
49+
);
50+
}
51+
52+
public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSide): Type
53+
{
54+
if (count($leftSide->getConstantArrays()) > 0 || count($rightSide->getConstantArrays()) > 0) {
55+
return new ErrorType();
56+
}
57+
58+
$possibleTypes = [
59+
new ObjectType('BcMath\Number'),
60+
];
61+
62+
$leftTypes = TypeUtils::flattenTypes(TypeCombinator::remove($leftSide, new ObjectType('BcMath\Number')));
63+
$rightTypes = TypeUtils::flattenTypes(TypeCombinator::remove($rightSide, new ObjectType('BcMath\Number')));
64+
$operator = self::OPERATORS[$operatorSigil];
65+
$contest = InitializerExprContext::createEmpty();
66+
67+
if (count($leftTypes) === 0 xor count($rightTypes) === 0) {
68+
$otherType = count($leftTypes) === 0 ? $rightSide : $leftSide;
69+
if ($otherType->isSuperTypeOf(new ObjectType('BcMath\Number'))->no()
70+
&& (
71+
!$otherType->isInteger()->no() || !$otherType->isFloat()->no() || !$otherType->isNumericString()->no()
72+
)
73+
) {
74+
return new ErrorType();
75+
}
76+
}
77+
78+
foreach ($leftTypes as $leftType) {
79+
foreach ($rightTypes as $rightType) {
80+
$node = new $operator(new TypeExpr($leftType), new TypeExpr($rightType));
81+
$possibleTypes[] = $this->InitializerExprTypeResolver->getType($node, $contest);
82+
}
83+
}
84+
85+
return TypeCombinator::union(...$possibleTypes);
86+
}
87+
88+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use Closure;
6+
use PHPStan\Fixture\TestDecimal;
7+
use PHPStan\Php\PhpVersion;
8+
use PHPStan\Testing\PHPStanTestCase;
9+
use PHPStan\Type\Constant\ConstantIntegerType;
10+
use PHPStan\Type\TypeCombinator;
11+
use PHPStan\Type\VerbosityLevel;
12+
use function sprintf;
13+
14+
class BcMathNumberOperatorTypeSpecifyingExtensionTest extends PHPStanTestCase
15+
{
16+
17+
/**
18+
* @dataProvider dataSigilAndSidesProvider
19+
*/
20+
public function test(string $sigil, Type $left, Type $right, string $expected): void
21+
{
22+
$extension = self::getContainer()->getByType(BcMathNumberOperatorTypeSpecifyingExtension::class);
23+
24+
$this->assertTrue($extension->isOperatorSupported($sigil, $left, $right));
25+
26+
$actualType = $extension->specifyType($sigil, $left, $right)->describe(VerbosityLevel::precise());
27+
$this->assertSame($expected, $actualType);
28+
}
29+
30+
public static function dataSigilAndSidesProvider(): iterable
31+
{
32+
$phpVersion = self::getContainer()->getByType(PhpVersion::class);
33+
if (!$phpVersion->supportsBcMathNumberOperatorOverloading()) {
34+
return;
35+
}
36+
37+
$supportedOperators = Closure::bind(
38+
static fn () => BcMathNumberOperatorTypeSpecifyingExtension::OPERATORS,
39+
null,
40+
BcMathNumberOperatorTypeSpecifyingExtension::class,
41+
)();
42+
foreach ($supportedOperators as $operator => $_) {
43+
yield sprintf('BcMath\Number %s BcMath\Number', $operator) => [
44+
'sigil' => $operator,
45+
'left' => new ObjectType('BcMath\Number'),
46+
'right' => new ObjectType('BcMath\Number'),
47+
'expected' => 'BcMath\Number',
48+
];
49+
}
50+
51+
$oneType = new ConstantIntegerType(1);
52+
53+
54+
yield 'BcMath\Number + 1' => [
55+
'sigil' => '+',
56+
'left' => new ObjectType('BcMath\Number'),
57+
'right' => new ConstantIntegerType(1),
58+
'expected' => 'BcMath\Number',
59+
];
60+
61+
yield 'BcMath\Number + 1' => [
62+
'sigil' => '+',
63+
'left' => new ObjectType('BcMath\Number'),
64+
'right' => TypeCombinator::union(
65+
new ObjectType('BcMath\Number'),
66+
new StringType(),
67+
),
68+
'expected' => 'BcMath\Number',
69+
];
70+
71+
yield '1|2|BcMath\Number + 3|4|BcMath\Number' => [
72+
'sigil' => '+',
73+
'left' => TypeCombinator::union(
74+
new ObjectType('BcMath\Number'),
75+
new ConstantIntegerType(1),
76+
new ConstantIntegerType(2),
77+
),
78+
'right' => TypeCombinator::union(
79+
new ObjectType('BcMath\Number'),
80+
new ConstantIntegerType(3),
81+
new ConstantIntegerType(4),
82+
),
83+
'expected' => '4|5|6|BcMath\Number',
84+
];
85+
86+
yield 'int|BcMath\Number + float|BcMath\Number' => [
87+
'sigil' => '+',
88+
'left' => TypeCombinator::union(
89+
new ObjectType('BcMath\Number'),
90+
new IntegerType(),
91+
),
92+
'right' => TypeCombinator::union(
93+
new ObjectType('BcMath\Number'),
94+
new IntegerType(),
95+
new FloatType(),
96+
),
97+
'expected' => 'BcMath\Number|float|int',
98+
];
99+
}
100+
101+
/**
102+
* @dataProvider dataNotMatchingSidesProvider
103+
*/
104+
public function testNotSupportsNotMatchingSides(string $sigil, Type $left, Type $right): void
105+
{
106+
$extension = self::getContainer()->getByType(BcMathNumberOperatorTypeSpecifyingExtension::class);
107+
108+
$this->assertFalse($extension->isOperatorSupported($sigil, $left, $right));
109+
}
110+
111+
public static function dataNotMatchingSidesProvider(): iterable
112+
{
113+
$phpVersion = self::getContainer()->getByType(PhpVersion::class);
114+
if (!$phpVersion->supportsBcMathNumberOperatorOverloading()) {
115+
$desc = sprintf("BcMath\Number is supported in PHP 8.4.0 and above. %s was given.", $phpVersion->getVersionString());
116+
yield $desc => [
117+
'sigil' => '+',
118+
'left' => new ObjectType('BcMath\Number'),
119+
'right' => new ObjectType('BcMath\Number'),
120+
];
121+
return;
122+
}
123+
124+
$notSupportedOperators = ['&', '|', '^', '||', '&&'];
125+
foreach ($notSupportedOperators as $notSupportedOperator) {
126+
yield sprintf('Do not support %s operator', $notSupportedOperator) => [
127+
$notSupportedOperator,
128+
new ObjectType('BcMath\Number'),
129+
new ObjectType('BcMath\Number'),
130+
];
131+
}
132+
133+
yield sprintf('Do not support %s operator', $notSupportedOperator) => [
134+
$notSupportedOperator,
135+
new ObjectType('BcMath\Number'),
136+
new ObjectType('BcMath\Number'),
137+
];
138+
}
139+
140+
}

0 commit comments

Comments
 (0)