diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 0a1d4ed2ca..b9506fc93b 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -80,11 +80,12 @@ use PHPStan\Type\VoidType; use Traversable; use function array_key_exists; -use function array_keys; use function array_map; use function count; use function get_class; use function in_array; +use function max; +use function min; use function preg_quote; use function str_replace; use function strpos; @@ -585,7 +586,7 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na return new ErrorType(); } elseif ($mainTypeName === 'int-mask-of') { if (count($genericTypes) === 1) { // int-mask-of - $maskType = $this->generateIntMaskType($genericTypes[0]); + $maskType = $this->expandIntMaskToType($genericTypes[0]); if ($maskType !== null) { return $maskType; } @@ -594,7 +595,7 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na return new ErrorType(); } elseif ($mainTypeName === 'int-mask') { if (count($genericTypes) > 0) { // int-mask<1, 2, 4> - $maskType = $this->generateIntMaskType(TypeCombinator::union(...$genericTypes)); + $maskType = $this->expandIntMaskToType(TypeCombinator::union(...$genericTypes)); if ($maskType !== null) { return $maskType; } @@ -853,7 +854,7 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc return new ErrorType(); } - private function generateIntMaskType(Type $type): ?Type + private function expandIntMaskToType(Type $type): ?Type { $ints = array_map(static fn (ConstantIntegerType $type) => $type->getValue(), TypeUtils::getConstantIntegers($type)); if (count($ints) === 0) { @@ -865,16 +866,24 @@ private function generateIntMaskType(Type $type): ?Type foreach ($ints as $int) { if ($int !== 0 && !array_key_exists($int, $values)) { foreach ($values as $value) { - $values[$value | $int] = true; + $computedValue = $value | $int; + $values[$computedValue] = $computedValue; } } - $values[$int] = true; + $values[$int] = $int; } - $values = array_map(static fn ($value) => new ConstantIntegerType($value), array_keys($values)); + $values[0] = 0; - return TypeCombinator::union(...$values); + $min = min($values); + $max = max($values); + + if ($max - $min === count($values) - 1) { + return IntegerRangeType::fromInterval($min, $max); + } + + return TypeCombinator::union(...array_map(static fn ($value) => new ConstantIntegerType($value), $values)); } /** diff --git a/tests/PHPStan/Analyser/data/int-mask.php b/tests/PHPStan/Analyser/data/int-mask.php index 54121aa28e..797d24f8d2 100644 --- a/tests/PHPStan/Analyser/data/int-mask.php +++ b/tests/PHPStan/Analyser/data/int-mask.php @@ -9,6 +9,9 @@ class HelloWorld const FOO_BAR = 1; const FOO_BAZ = 2; + const BAZ_FOO = 1; + const BAZ_BAR = 4; + const BAR_INT = 1; const BAR_STR = ''; @@ -16,16 +19,16 @@ class HelloWorld * @param int-mask-of $one * @param int-mask $two * @param int-mask<1, 2, 8> $three - * @param 0|int-mask<1, 2, 8> $four - * @param int-mask<1, 4, 16, 64, 256, 1024> $five + * @param int-mask<1, 4, 16, 64, 256, 1024> $four + * @param int-mask-of $five */ public static function test(int $one, int $two, int $three, int $four, int $five): void { - assertType('1|2|3', $one); - assertType('1|2|3', $two); - assertType('1|2|3|8|9|10|11', $three); - assertType('0|1|2|3|8|9|10|11', $four); - assertType('1|4|5|16|17|20|21|64|65|68|69|80|81|84|85|256|257|260|261|272|273|276|277|320|321|324|325|336|337|340|341|1024|1025|1028|1029|1040|1041|1044|1045|1088|1089|1092|1093|1104|1105|1108|1109|1280|1281|1284|1285|1296|1297|1300|1301|1344|1345|1348|1349|1360|1361|1364|1365', $five); + assertType('int<0, 3>', $one); + assertType('int<0, 3>', $two); + assertType('0|1|2|3|8|9|10|11', $three); + assertType('0|1|4|5|16|17|20|21|64|65|68|69|80|81|84|85|256|257|260|261|272|273|276|277|320|321|324|325|336|337|340|341|1024|1025|1028|1029|1040|1041|1044|1045|1088|1089|1092|1093|1104|1105|1108|1109|1280|1281|1284|1285|1296|1297|1300|1301|1344|1345|1348|1349|1360|1361|1364|1365', $four); + assertType('0|1|4|5', $five); } /**