diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 19291fdae29..802a0e3df97 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -171,7 +171,8 @@ public static function union(Type ...$types): Type } if ($types[$i] instanceof ConstantScalarType) { $type = $types[$i]; - $scalarTypes[get_class($type)][md5($type->describe(VerbosityLevel::cache()))] = $type; + $key = $type instanceof ConstantIntegerType ? $type->getValue() : md5($type->describe(VerbosityLevel::cache())); + $scalarTypes[get_class($type)][$key] = $type; unset($types[$i]); continue; } @@ -256,6 +257,33 @@ public static function union(Type ...$types): Type $types[] = new BooleanType(); continue; } + if ($classType === ConstantIntegerType::class) { + /** @var int[] */ + $integers = array_keys($scalarTypeItems); + sort($integers); + /** @var ConstantIntegerType[] */ + $loneIntegers = []; + $rangeStart = null; + $rangeEnd = null; + for ($i = 0; $i < count($integers); $i++) { + if ($i < count($integers) - 1 && $integers[$i] + 1 === $integers[$i + 1]) { + $rangeStart ??= $integers[$i]; + $rangeEnd = $integers[$i + 1]; + } elseif ($rangeStart === null) { + $loneIntegers[] = $scalarTypeItems[$integers[$i]]; + } else { + $types[] = IntegerRangeType::fromInterval($rangeStart, $rangeEnd); + $rangeStart = null; + $rangeEnd = null; + } + } + if (count($loneIntegers) > self::CONSTANT_SCALAR_UNION_THRESHOLD) { + $types[] = new IntegerType(); + } else { + array_push($types, ...$loneIntegers); + } + continue; + } foreach ($scalarTypeItems as $type) { if (count($scalarTypeItems) > self::CONSTANT_SCALAR_UNION_THRESHOLD) { $types[] = $type->generalize(); diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 62bf98915ba..b99531a8ea9 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -193,6 +193,10 @@ public function testInstantiation(): void 'Class TestInstantiation\ClassExtendingAbstractConstructor constructor invoked with 0 parameters, 1 required.', 273, ], + [ + 'Parameter #2 $y of class TestInstantiation\IntRange constructor expects int<1,7>, int<1, 8> given.', + 291, + ], ] ); } diff --git a/tests/PHPStan/Rules/Classes/data/instantiation.php b/tests/PHPStan/Rules/Classes/data/instantiation.php index 58c5b2cf60c..ae3aef80f08 100644 --- a/tests/PHPStan/Rules/Classes/data/instantiation.php +++ b/tests/PHPStan/Rules/Classes/data/instantiation.php @@ -274,3 +274,29 @@ public function doBar() } } + +final class IntRange +{ + /** + * @psalm-var 1|2|3|4|5|6|7|8 + */ + private int $x; + + public static function fromInt(int $x): self + { + if ($x < 1 || $x > 8) { + throw new InvalidArgumentException; + } + + return new self($x, $x); + } + + /** + * @psalm-param 1|2|3|4|5|6|7|8 $x + * @psalm-param 1|2|3|4|5|6|7 $y + */ + private function __construct(int $x, int $y) + { + $y = $this->x = $x; + } +} diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index 3781881114b..3e454fe1934 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -61,6 +61,10 @@ public function testReturnTypeRule(): void 'Function ReturnTypes\returnNever() should never return but return statement found.', 181, ], + [ + 'Function ReturnTypes\returnRangeBad() should return int<1, 7> but returns int<1, 8>.', + 203, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/returnTypes.php b/tests/PHPStan/Rules/Functions/data/returnTypes.php index 3e15651c697..49355cc5a2d 100644 --- a/tests/PHPStan/Rules/Functions/data/returnTypes.php +++ b/tests/PHPStan/Rules/Functions/data/returnTypes.php @@ -180,3 +180,25 @@ function returnNever() { return; } + +/** + * @return 1|2|3|4|5|6|7|8 + */ +function returnRange(int $x) : int { + if ($x < 1 || $x > 8) { + throw new InvalidArgumentException; + } + + return $x; +} + +/** + * @return 1|2|3|4|5|6|7 + */ +function returnRangeBad(int $x) : int { + if ($x < 1 || $x > 8) { + throw new InvalidArgumentException; + } + + return $x; +} diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index a42c4e26009..5d132c7e2c6 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -1760,6 +1760,33 @@ public function dataUnion(): array MixedType::class, 'mixed=implicit', ], + [ + [ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + new ConstantIntegerType(4), + new ConstantIntegerType(5), + new ConstantIntegerType(6), + ], + IntegerRangeType::class, + 'int<1,6>', + ], + [ + [ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + new ConstantIntegerType(5), + new ConstantIntegerType(7), + new ConstantIntegerType(8), + new ConstantIntegerType(9), + new ConstantIntegerType(10), + new ConstantIntegerType(11), + ], + UnionType::class, + 'int<1,5>|7|int<8,11>', + ], ]; }