Skip to content

Commit cd50c21

Browse files
committed
Improve abs() return type
1 parent c4c0269 commit cd50c21

File tree

3 files changed

+268
-0
lines changed

3 files changed

+268
-0
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,11 @@ services:
11081108
-
11091109
class: PHPStan\Type\BitwiseFlagHelper
11101110

1111+
-
1112+
class: PHPStan\Type\Php\AbsFunctionDynamicReturnTypeExtension
1113+
tags:
1114+
- phpstan.broker.dynamicFunctionReturnTypeExtension
1115+
11111116
-
11121117
class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension
11131118
tags:
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Type\Constant\ConstantFloatType;
9+
use PHPStan\Type\Constant\ConstantIntegerType;
10+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
11+
use PHPStan\Type\IntegerRangeType;
12+
use PHPStan\Type\Type;
13+
use PHPStan\Type\UnionType;
14+
use function abs;
15+
use function count;
16+
use function max;
17+
18+
class AbsFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
19+
{
20+
21+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
22+
{
23+
return $functionReflection->getName() === 'abs';
24+
}
25+
26+
public function getTypeFromFunctionCall(
27+
FunctionReflection $functionReflection,
28+
FuncCall $functionCall,
29+
Scope $scope,
30+
): ?Type
31+
{
32+
$args = $functionCall->getArgs();
33+
34+
if (!isset($args[0])) {
35+
return null;
36+
}
37+
38+
$type = $scope->getType($args[0]->value);
39+
40+
if ($type instanceof UnionType) {
41+
$ranges = [];
42+
43+
foreach ($type->getTypes() as $unionType) {
44+
if (
45+
!$unionType instanceof ConstantIntegerType
46+
&& !$unionType instanceof IntegerRangeType
47+
&& !$unionType instanceof ConstantFloatType
48+
) {
49+
return null;
50+
}
51+
52+
$absRange = $this->absType($unionType);
53+
54+
foreach ($ranges as $index => $range) {
55+
if (!($range instanceof IntegerRangeType)) {
56+
continue;
57+
}
58+
59+
$unionRange = $range->tryUnion($absRange);
60+
61+
if ($unionRange !== null) {
62+
$ranges[$index] = $unionRange;
63+
64+
continue 2;
65+
}
66+
}
67+
68+
$ranges[] = $absRange;
69+
}
70+
71+
if (count($ranges) === 1) {
72+
return $ranges[0];
73+
}
74+
75+
return new UnionType($ranges);
76+
}
77+
78+
if (
79+
$type instanceof ConstantIntegerType
80+
|| $type instanceof IntegerRangeType
81+
|| $type instanceof ConstantFloatType
82+
) {
83+
return $this->absType($type);
84+
}
85+
86+
return null;
87+
}
88+
89+
private function absType(ConstantIntegerType|IntegerRangeType|ConstantFloatType $type): Type
90+
{
91+
if ($type instanceof ConstantIntegerType) {
92+
return new ConstantIntegerType(abs($type->getValue()));
93+
}
94+
95+
if ($type instanceof ConstantFloatType) {
96+
return new ConstantFloatType(abs($type->getValue()));
97+
}
98+
99+
$min = $type->getMin();
100+
$max = $type->getMax();
101+
102+
if ($min !== null && $min >= 0) {
103+
return IntegerRangeType::fromInterval($min, $max);
104+
}
105+
106+
if ($max === null || $max >= 0) {
107+
$inversedMin = $min !== null ? $min * -1 : null;
108+
109+
return IntegerRangeType::fromInterval(0, $inversedMin !== null && $max !== null ? max($inversedMin, $max) : null);
110+
}
111+
112+
return IntegerRangeType::fromInterval($max * -1, $min !== null ? $min * -1 : null);
113+
}
114+
115+
}

tests/PHPStan/Analyser/nsrt/abs.php

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Abs;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
public function singleIntegerRange(int $int): void
11+
{
12+
/** @var int $int */
13+
assertType('int<0, max>', abs($int));
14+
15+
/** @var positive-int $int */
16+
assertType('int<1, max>', abs($int));
17+
18+
/** @var negative-int $int */
19+
assertType('int<1, max>', abs($int));
20+
21+
/** @var non-negative-int $int */
22+
assertType('int<0, max>', abs($int));
23+
24+
/** @var non-positive-int $int */
25+
assertType('int<0, max>', abs($int));
26+
27+
/** @var int<0, max> $int */
28+
assertType('int<0, max>', abs($int));
29+
30+
/** @var int<0, 123> $int */
31+
assertType('int<0, 123>', abs($int));
32+
33+
/** @var int<-123, 0> $int */
34+
assertType('int<0, 123>', abs($int));
35+
36+
/** @var int<1, max> $int */
37+
assertType('int<1, max>', abs($int));
38+
39+
/** @var int<123, max> $int */
40+
assertType('int<123, max>', abs($int));
41+
42+
/** @var int<123, 456> $int */
43+
assertType('int<123, 456>', abs($int));
44+
45+
/** @var int<min, 0> $int */
46+
assertType('int<0, max>', abs($int));
47+
48+
/** @var int<min, -1> $int */
49+
assertType('int<1, max>', abs($int));
50+
51+
/** @var int<min, -123> $int */
52+
assertType('int<123, max>', abs($int));
53+
54+
/** @var int<-456, -123> $int */
55+
assertType('int<123, 456>', abs($int));
56+
57+
/** @var int<-123, 123> $int */
58+
assertType('int<0, 123>', abs($int));
59+
60+
/** @var int<min, max> $int */
61+
assertType('int<0, max>', abs($int));
62+
}
63+
64+
public function multipleIntegerRanges(int $int): void
65+
{
66+
/** @var non-zero-int $int */
67+
assertType('int<1, max>', abs($int));
68+
69+
/** @var int<min, -1>|int<1, max> $int */
70+
assertType('int<1, max>', abs($int));
71+
72+
/** @var int<-20, -10>|int<5, 25> $int */
73+
assertType('int<5, 25>', abs($int));
74+
75+
/** @var int<-20, -5>|int<10, 25> $int */
76+
assertType('int<5, 25>', abs($int));
77+
78+
/** @var int<-25, -10>|int<5, 20> $int */
79+
assertType('int<5, 25>', abs($int));
80+
81+
/** @var int<-20, -10>|int<20, 30> $int */
82+
assertType('int<10, 30>', abs($int));
83+
}
84+
85+
public function constantInteger(int $int): void
86+
{
87+
/** @var 0 $int */
88+
assertType('0', abs($int));
89+
90+
/** @var 1 $int */
91+
assertType('1', abs($int));
92+
93+
/** @var -1 $int */
94+
assertType('1', abs($int));
95+
}
96+
97+
public function mixedIntegerUnion(int $int): void
98+
{
99+
/** @var 123|int<456, max> $int */
100+
assertType('123|int<456, max>', abs($int));
101+
102+
/** @var int<min, -456>|-123 $int */
103+
assertType('123|int<456, max>', abs($int));
104+
105+
/** @var -123|int<124, 125> $int */
106+
assertType('int<123, 125>', abs($int));
107+
108+
/** @var int<124, 125>|-123 $int */
109+
assertType('int<123, 125>', abs($int));
110+
}
111+
112+
public function constantFloat(float $float): void
113+
{
114+
/** @var 0.0 $float */
115+
assertType('0.0', abs($float));
116+
117+
/** @var 1.0 $float */
118+
assertType('1.0', abs($float));
119+
120+
/** @var -1.0 $float */
121+
assertType('1.0', abs($float));
122+
}
123+
124+
public function mixedUnion(float $float): void
125+
{
126+
/** @var 1.0|int<2, 3> $float */
127+
assertType('1.0|int<2, 3>', abs($float));
128+
129+
/** @var -1.0|int<-3, -2> $float */
130+
assertType('1.0|int<2, 3>', abs($float));
131+
132+
/** @var 2.0|int<1, 3> $float */
133+
assertType('2.0|int<1, 3>', abs($float));
134+
135+
/** @var -2.0|int<-3, -1> $float */
136+
assertType('2.0|int<1, 3>', abs($float));
137+
}
138+
139+
public function invalidType(mixed $nonInt): void
140+
{
141+
/** @var string $nonInt */
142+
assertType('float|int<0, max>', abs($nonInt));
143+
144+
/** @var string|positive-int $nonInt */
145+
assertType('float|int<0, max>', abs($nonInt));
146+
}
147+
148+
}

0 commit comments

Comments
 (0)