Skip to content

Commit

Permalink
Make IntegerRangeType->isSubTypeOf analyse Unions
Browse files Browse the repository at this point in the history
Partially resolves phpstan/phpstan#3339
  • Loading branch information
GKFX committed Jan 3, 2021
1 parent 69e68a7 commit 40ea36d
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 4 deletions.
16 changes: 16 additions & 0 deletions src/Type/ImplementsSubTypeOfUnion.php
@@ -0,0 +1,16 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PHPStan\TrinaryLogic;

/**
* Indictates that isSubTypeOf may be called with a UnionType without risking
* infinite recursion.
*/
interface ImplementsSubTypeOfUnion
{

public function isSubTypeOf(Type $type): TrinaryLogic;

}
51 changes: 49 additions & 2 deletions src/Type/IntegerRangeType.php
Expand Up @@ -6,7 +6,7 @@
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;

class IntegerRangeType extends IntegerType implements CompoundType
class IntegerRangeType extends IntegerType implements CompoundType, ImplementsSubTypeOfUnion
{

private ?int $min;
Expand Down Expand Up @@ -156,13 +156,60 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic
return $otherType->isSuperTypeOf($this);
}

if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) {
if ($otherType instanceof IntersectionType) {
return $otherType->isSuperTypeOf($this);
}

if ($otherType instanceof UnionType) {
/** @var array<array{int,int}> */
$otherRanges = [];
foreach ($otherType->getTypes() as $component) {
if (get_class($component) === IntegerType::class) {
return TrinaryLogic::createYes();
}
if ($component instanceof ConstantIntegerType) {
$otherRanges[] = [$component->getValue(), $component->getValue()];
} elseif ($component instanceof self) {
$otherRanges[] = [$component->getMin() ?? PHP_INT_MIN, $component->getMax() ?? PHP_INT_MAX];
}
}
self::unionIntRanges($otherRanges);
foreach ($otherRanges as $otherRange) {
// The $otherRanges are disjoint so only one of these conditions
// will ever match.
if ($otherRange[0] <= ($this->getMin() ?? PHP_INT_MIN) &&
$otherRange[1] >= ($this->getMax() ?? PHP_INT_MAX)) {
return TrinaryLogic::createYes();
}
if (!self::isDisjoint($this->getMin(), $this->getMax(), ...$otherRange)) {
return TrinaryLogic::createMaybe();
}
}
}

return TrinaryLogic::createNo();
}

/**
* Take the union of a list of ranges, merging all adjacent ranges.
* @param array<array{int,int}> $ranges A list of closed, integer intervals.
*/
private static function unionIntRanges(array &$ranges): void
{
usort($ranges, static function ($a, $b): int {
return $a[0] <=> $b[0];
});
for ($i = 0; $i < count($ranges) - 1;) {
$thisMax = $ranges[$i][1];
if ($thisMax === PHP_INT_MAX || $thisMax + 1 >= $ranges[$i + 1][0]) {
$ranges[$i][1] = max($ranges[$i][1], $ranges[$i + 1][1]);
array_splice($ranges, $i + 1, 1);
} else {
$i++;
}
}
}

public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic
{
return $this->isSubTypeOf($acceptingType);
Expand Down
2 changes: 1 addition & 1 deletion src/Type/IterableType.php
Expand Up @@ -14,7 +14,7 @@
use PHPStan\Type\Traits\UndecidedBooleanTypeTrait;
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;

class IterableType implements CompoundType
class IterableType implements CompoundType, ImplementsSubTypeOfUnion
{

use MaybeCallableTypeTrait;
Expand Down
2 changes: 1 addition & 1 deletion src/Type/UnionType.php
Expand Up @@ -77,7 +77,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic

public function isSuperTypeOf(Type $otherType): TrinaryLogic
{
if ($otherType instanceof self || $otherType instanceof IterableType) {
if ($otherType instanceof self || $otherType instanceof ImplementsSubTypeOfUnion) {
return $otherType->isSubTypeOf($this);
}

Expand Down
75 changes: 75 additions & 0 deletions tests/PHPStan/Type/IntegerRangeTypeTest.php
@@ -0,0 +1,75 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PHPStan\TrinaryLogic;
use PHPStan\Type\Constant\ConstantIntegerType;

class IntegerRangeTypeTest extends \PHPStan\Testing\TestCase
{

public function dataIsSubTypeOf(): iterable
{
yield [
IntegerRangeType::fromInterval(5, 10),
new IntegerType(),
TrinaryLogic::createYes(),
];

yield [
IntegerRangeType::fromInterval(5, 10),
new ConstantIntegerType(10),
TrinaryLogic::createMaybe(),
];

yield [
IntegerRangeType::fromInterval(5, 10),
new ConstantIntegerType(20),
TrinaryLogic::createNo(),
];

yield [
IntegerRangeType::fromInterval(5, 10),
IntegerRangeType::fromInterval(0, 15),
TrinaryLogic::createYes(),
];

yield [
IntegerRangeType::fromInterval(5, 10),
IntegerRangeType::fromInterval(6, 9),
TrinaryLogic::createMaybe(),
];

yield [
IntegerRangeType::fromInterval(5, 10),
new UnionType([new IntegerType(), new StringType()]),
TrinaryLogic::createYes(),
];

yield [
IntegerRangeType::fromInterval(5, 10),
new UnionType([new ConstantIntegerType(5), IntegerRangeType::fromInterval(6, 10), new StringType()]),
TrinaryLogic::createYes(),
];

yield [
IntegerRangeType::fromInterval(5, 10),
new StringType(),
TrinaryLogic::createNo(),
];
}

/**
* @dataProvider dataIsSubTypeOf
*/
public function testIsSubTypeOf(IntegerRangeType $type, Type $otherType, TrinaryLogic $expectedResult): void
{
$actualResult = $type->isSubTypeOf($otherType);
$this->assertSame(
$expectedResult->describe(),
$actualResult->describe(),
sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise()))
);
}

}

0 comments on commit 40ea36d

Please sign in to comment.