From 40ea36d280acfba4594e7e41b4c9b154c898d7cc Mon Sep 17 00:00:00 2001 From: George Bateman Date: Sun, 3 Jan 2021 22:05:03 +0000 Subject: [PATCH] Make IntegerRangeType->isSubTypeOf analyse Unions Partially resolves https://github.com/phpstan/phpstan/issues/3339 --- src/Type/ImplementsSubTypeOfUnion.php | 16 +++++ src/Type/IntegerRangeType.php | 51 +++++++++++++- src/Type/IterableType.php | 2 +- src/Type/UnionType.php | 2 +- tests/PHPStan/Type/IntegerRangeTypeTest.php | 75 +++++++++++++++++++++ 5 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 src/Type/ImplementsSubTypeOfUnion.php create mode 100644 tests/PHPStan/Type/IntegerRangeTypeTest.php diff --git a/src/Type/ImplementsSubTypeOfUnion.php b/src/Type/ImplementsSubTypeOfUnion.php new file mode 100644 index 00000000000..268dcceaa54 --- /dev/null +++ b/src/Type/ImplementsSubTypeOfUnion.php @@ -0,0 +1,16 @@ +isSuperTypeOf($this); } - if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + if ($otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } + if ($otherType instanceof UnionType) { + /** @var array */ + $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 $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); diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index e1de4d5fc4a..b2875792ade 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -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; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index b9440dc5549..9e32065dcf5 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -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); } diff --git a/tests/PHPStan/Type/IntegerRangeTypeTest.php b/tests/PHPStan/Type/IntegerRangeTypeTest.php new file mode 100644 index 00000000000..3cd6e410f84 --- /dev/null +++ b/tests/PHPStan/Type/IntegerRangeTypeTest.php @@ -0,0 +1,75 @@ +isSubTypeOf($otherType); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + ); + } + +}