Skip to content

Commit

Permalink
Improve abs() return type
Browse files Browse the repository at this point in the history
  • Loading branch information
julienfalque committed Jul 24, 2024
1 parent c4c0269 commit cd50c21
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 0 deletions.
5 changes: 5 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,11 @@ services:
-
class: PHPStan\Type\BitwiseFlagHelper

-
class: PHPStan\Type\Php\AbsFunctionDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension
tags:
Expand Down
115 changes: 115 additions & 0 deletions src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Constant\ConstantFloatType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use function abs;
use function count;
use function max;

class AbsFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'abs';
}

public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope,
): ?Type
{
$args = $functionCall->getArgs();

if (!isset($args[0])) {
return null;
}

$type = $scope->getType($args[0]->value);

if ($type instanceof UnionType) {
$ranges = [];

foreach ($type->getTypes() as $unionType) {
if (
!$unionType instanceof ConstantIntegerType
&& !$unionType instanceof IntegerRangeType
&& !$unionType instanceof ConstantFloatType
) {
return null;
}

$absRange = $this->absType($unionType);

foreach ($ranges as $index => $range) {
if (!($range instanceof IntegerRangeType)) {
continue;
}

$unionRange = $range->tryUnion($absRange);

if ($unionRange !== null) {
$ranges[$index] = $unionRange;

continue 2;
}
}

$ranges[] = $absRange;
}

if (count($ranges) === 1) {
return $ranges[0];
}

return new UnionType($ranges);
}

if (
$type instanceof ConstantIntegerType
|| $type instanceof IntegerRangeType
|| $type instanceof ConstantFloatType
) {
return $this->absType($type);
}

return null;
}

private function absType(ConstantIntegerType|IntegerRangeType|ConstantFloatType $type): Type
{
if ($type instanceof ConstantIntegerType) {
return new ConstantIntegerType(abs($type->getValue()));
}

if ($type instanceof ConstantFloatType) {
return new ConstantFloatType(abs($type->getValue()));
}

$min = $type->getMin();
$max = $type->getMax();

if ($min !== null && $min >= 0) {
return IntegerRangeType::fromInterval($min, $max);
}

if ($max === null || $max >= 0) {
$inversedMin = $min !== null ? $min * -1 : null;

return IntegerRangeType::fromInterval(0, $inversedMin !== null && $max !== null ? max($inversedMin, $max) : null);
}

return IntegerRangeType::fromInterval($max * -1, $min !== null ? $min * -1 : null);
}

}
148 changes: 148 additions & 0 deletions tests/PHPStan/Analyser/nsrt/abs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php declare(strict_types = 1);

namespace Abs;

use function PHPStan\Testing\assertType;

class Foo
{

public function singleIntegerRange(int $int): void
{
/** @var int $int */
assertType('int<0, max>', abs($int));

/** @var positive-int $int */
assertType('int<1, max>', abs($int));

/** @var negative-int $int */
assertType('int<1, max>', abs($int));

/** @var non-negative-int $int */
assertType('int<0, max>', abs($int));

/** @var non-positive-int $int */
assertType('int<0, max>', abs($int));

/** @var int<0, max> $int */
assertType('int<0, max>', abs($int));

/** @var int<0, 123> $int */
assertType('int<0, 123>', abs($int));

/** @var int<-123, 0> $int */
assertType('int<0, 123>', abs($int));

/** @var int<1, max> $int */
assertType('int<1, max>', abs($int));

/** @var int<123, max> $int */
assertType('int<123, max>', abs($int));

/** @var int<123, 456> $int */
assertType('int<123, 456>', abs($int));

/** @var int<min, 0> $int */
assertType('int<0, max>', abs($int));

/** @var int<min, -1> $int */
assertType('int<1, max>', abs($int));

/** @var int<min, -123> $int */
assertType('int<123, max>', abs($int));

/** @var int<-456, -123> $int */
assertType('int<123, 456>', abs($int));

/** @var int<-123, 123> $int */
assertType('int<0, 123>', abs($int));

/** @var int<min, max> $int */
assertType('int<0, max>', abs($int));
}

public function multipleIntegerRanges(int $int): void
{
/** @var non-zero-int $int */
assertType('int<1, max>', abs($int));

/** @var int<min, -1>|int<1, max> $int */
assertType('int<1, max>', abs($int));

/** @var int<-20, -10>|int<5, 25> $int */
assertType('int<5, 25>', abs($int));

/** @var int<-20, -5>|int<10, 25> $int */
assertType('int<5, 25>', abs($int));

/** @var int<-25, -10>|int<5, 20> $int */
assertType('int<5, 25>', abs($int));

/** @var int<-20, -10>|int<20, 30> $int */
assertType('int<10, 30>', abs($int));
}

public function constantInteger(int $int): void
{
/** @var 0 $int */
assertType('0', abs($int));

/** @var 1 $int */
assertType('1', abs($int));

/** @var -1 $int */
assertType('1', abs($int));
}

public function mixedIntegerUnion(int $int): void
{
/** @var 123|int<456, max> $int */
assertType('123|int<456, max>', abs($int));

/** @var int<min, -456>|-123 $int */
assertType('123|int<456, max>', abs($int));

/** @var -123|int<124, 125> $int */
assertType('int<123, 125>', abs($int));

/** @var int<124, 125>|-123 $int */
assertType('int<123, 125>', abs($int));
}

public function constantFloat(float $float): void
{
/** @var 0.0 $float */
assertType('0.0', abs($float));

/** @var 1.0 $float */
assertType('1.0', abs($float));

/** @var -1.0 $float */
assertType('1.0', abs($float));
}

public function mixedUnion(float $float): void
{
/** @var 1.0|int<2, 3> $float */
assertType('1.0|int<2, 3>', abs($float));

/** @var -1.0|int<-3, -2> $float */
assertType('1.0|int<2, 3>', abs($float));

/** @var 2.0|int<1, 3> $float */
assertType('2.0|int<1, 3>', abs($float));

/** @var -2.0|int<-3, -1> $float */
assertType('2.0|int<1, 3>', abs($float));
}

public function invalidType(mixed $nonInt): void
{
/** @var string $nonInt */
assertType('float|int<0, max>', abs($nonInt));

/** @var string|positive-int $nonInt */
assertType('float|int<0, max>', abs($nonInt));
}

}

0 comments on commit cd50c21

Please sign in to comment.