diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index f80eb0dc0d..b9506fc93b 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -79,10 +79,13 @@ use PHPStan\Type\UnionType; use PHPStan\Type\VoidType; use Traversable; +use function array_key_exists; 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; @@ -580,6 +583,24 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na return $genericTypes[0]->getIterableValueType(); } + return new ErrorType(); + } elseif ($mainTypeName === 'int-mask-of') { + if (count($genericTypes) === 1) { // int-mask-of + $maskType = $this->expandIntMaskToType($genericTypes[0]); + if ($maskType !== null) { + return $maskType; + } + } + + return new ErrorType(); + } elseif ($mainTypeName === 'int-mask') { + if (count($genericTypes) > 0) { // int-mask<1, 2, 4> + $maskType = $this->expandIntMaskToType(TypeCombinator::union(...$genericTypes)); + if ($maskType !== null) { + return $maskType; + } + } + return new ErrorType(); } elseif ($mainTypeName === '__benevolent') { if (count($genericTypes) === 1) { @@ -833,6 +854,38 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc return new ErrorType(); } + private function expandIntMaskToType(Type $type): ?Type + { + $ints = array_map(static fn (ConstantIntegerType $type) => $type->getValue(), TypeUtils::getConstantIntegers($type)); + if (count($ints) === 0) { + return null; + } + + $values = []; + + foreach ($ints as $int) { + if ($int !== 0 && !array_key_exists($int, $values)) { + foreach ($values as $value) { + $computedValue = $value | $int; + $values[$computedValue] = $computedValue; + } + } + + $values[$int] = $int; + } + + $values[0] = 0; + + $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)); + } + /** * @api * @param TypeNode[] $typeNodes diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index baa70858b6..0f3bcfd30d 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -5,6 +5,7 @@ use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use function array_merge; @@ -90,6 +91,14 @@ public static function getConstantStrings(Type $type): array return self::map(ConstantStringType::class, $type, false); } + /** + * @return ConstantIntegerType[] + */ + public static function getConstantIntegers(Type $type): array + { + return self::map(ConstantIntegerType::class, $type, false); + } + /** * @return ConstantType[] */ diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 0a4958f393..849385560f 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -659,6 +659,12 @@ public function testBug4308(): void $this->assertNoErrors($errors); } + public function testBug4732(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-4732.php'); + $this->assertNoErrors($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 7116c11e95..d4084c82a8 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -866,6 +866,8 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-optional-set.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6383.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-3284.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/int-mask.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-4732.php b/tests/PHPStan/Analyser/data/bug-4732.php new file mode 100644 index 0000000000..46f401919e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4732.php @@ -0,0 +1,23 @@ + $flags bitflags options + */ + public static function sayHello(int $flags): void + { + } + + public static function test(): void + { + HelloWorld::sayHello(HelloWorld::FOO_BAR | HelloWorld::FOO_BAZ); + HelloWorld::sayHello(HelloWorld::FOO_BAR); + HelloWorld::sayHello(HelloWorld::FOO_BAZ); + } +} diff --git a/tests/PHPStan/Analyser/data/int-mask.php b/tests/PHPStan/Analyser/data/int-mask.php new file mode 100644 index 0000000000..797d24f8d2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/int-mask.php @@ -0,0 +1,43 @@ + $one + * @param int-mask $two + * @param int-mask<1, 2, 8> $three + * @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('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); + } + + /** + * @param int-mask-of $one + * @param int-mask<0, 1, false> $two + */ + public static function invalid(int $one, int $two, int $three): void + { + assertType('int', $one); // not all constant integers + assertType('int', $two); // not all constant integers + } +}