diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index 3acf6fe213..88f7e68aad 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -5,8 +5,10 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use function array_filter; use function assert; use function ceil; +use function count; use function floor; use function get_class; use function is_int; @@ -244,13 +246,33 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic return $otherType->isSuperTypeOf($this); } - if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + if ($otherType instanceof UnionType) { + return $this->isSubTypeOfUnion($otherType); + } + + if ($otherType instanceof IntersectionType) { return $otherType->isSuperTypeOf($this); } return TrinaryLogic::createNo(); } + private function isSubTypeOfUnion(UnionType $otherType): TrinaryLogic + { + if ($this->min !== null && $this->max !== null) { + $matchingConstantIntegers = array_filter( + $otherType->getTypes(), + fn (Type $type): bool => $type instanceof ConstantIntegerType && $type->getValue() >= $this->min && $type->getValue() <= $this->max, + ); + + if (count($matchingConstantIntegers) === ($this->max - $this->min + 1)) { + return TrinaryLogic::createYes(); + } + } + + return TrinaryLogic::createNo()->lazyOr($otherType->getTypes(), fn (Type $innerType) => $this->isSubTypeOf($innerType)); + } + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { return $this->isSubTypeOf($acceptingType); diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 41f90bb02f..e27aa87217 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -133,6 +133,7 @@ public function isSuperTypeOf(Type $otherType): TrinaryLogic || $otherType instanceof NeverType || $otherType instanceof ConditionalType || $otherType instanceof ConditionalTypeForParameter + || $otherType instanceof IntegerRangeType ) { return $otherType->isSubTypeOf($this); } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 7e78d519a7..a9ac5b684d 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -445,4 +445,10 @@ public function testBug4680(): void $this->analyse([__DIR__ . '/data/bug-4680.php'], []); } + public function testBug3383(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-3383.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-3383.php b/tests/PHPStan/Rules/Properties/data/bug-3383.php new file mode 100644 index 0000000000..6ba907d2bb --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3383.php @@ -0,0 +1,13 @@ +classification = random_int(0, 3); + } +}