From 715b3f699ae919e56258003b4c3af12f8407d663 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Sat, 2 Apr 2022 14:44:57 +0200 Subject: [PATCH 1/5] Add support for int-mask and int-mask-of types --- src/PhpDoc/TypeNodeResolver.php | 44 +++++++++++++++++++ src/Type/TypeUtils.php | 9 ++++ .../Analyser/AnalyserIntegrationTest.php | 6 +++ .../Analyser/NodeScopeResolverTest.php | 2 + tests/PHPStan/Analyser/data/bug-4732.php | 23 ++++++++++ tests/PHPStan/Analyser/data/int-mask.php | 40 +++++++++++++++++ 6 files changed, 124 insertions(+) create mode 100644 tests/PHPStan/Analyser/data/bug-4732.php create mode 100644 tests/PHPStan/Analyser/data/int-mask.php diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index f80eb0dc0d..c3c742b43f 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -580,6 +580,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->generateIntMaskType($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->generateIntMaskType(TypeCombinator::union(...$genericTypes)); + if ($maskType !== null) { + return $maskType; + } + } + return new ErrorType(); } elseif ($mainTypeName === '__benevolent') { if (count($genericTypes) === 1) { @@ -833,6 +851,32 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc return new ErrorType(); } + private function generateIntMaskType(Type $type): ?Type + { + $ints = TypeUtils::getConstantIntegers($type); + if (!$ints) { + return null; + } + + $values = []; + + foreach ($ints as $int) { + $int = $int->getValue(); + + if ($int !== 0) { + foreach ($values as $value) { + $values[] = $value | $int; + } + } + + $values[] = $int; + } + + $values = array_map(static fn ($value) => new ConstantIntegerType($value), array_unique($values)); + + return TypeCombinator::union(...$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 d0e55dfdc7..6550b29665 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -860,6 +860,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..54121aa28e --- /dev/null +++ b/tests/PHPStan/Analyser/data/int-mask.php @@ -0,0 +1,40 @@ + $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 + */ + 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); + } + + /** + * @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 + } +} From 985358801e1f74c30550e59c5311c7c6d8d92c04 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Sat, 2 Apr 2022 14:46:31 +0200 Subject: [PATCH 2/5] cs --- src/PhpDoc/TypeNodeResolver.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index c3c742b43f..f177f3d568 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -80,6 +80,7 @@ use PHPStan\Type\VoidType; use Traversable; use function array_map; +use function array_unique; use function count; use function get_class; use function in_array; From 986b8f65199208f0fc266ae036f825a79f9a3a74 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Sat, 2 Apr 2022 14:49:54 +0200 Subject: [PATCH 3/5] cs --- src/PhpDoc/TypeNodeResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index f177f3d568..80c9e65d3e 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -855,7 +855,7 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc private function generateIntMaskType(Type $type): ?Type { $ints = TypeUtils::getConstantIntegers($type); - if (!$ints) { + if (count($ints) === 0) { return null; } From 2d7ba249479d4e5140274446adb0e247e0cc9ae6 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Sun, 3 Apr 2022 13:55:28 +0200 Subject: [PATCH 4/5] Optimize --- src/PhpDoc/TypeNodeResolver.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 80c9e65d3e..0a1d4ed2ca 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -79,8 +79,9 @@ use PHPStan\Type\UnionType; use PHPStan\Type\VoidType; use Traversable; +use function array_key_exists; +use function array_keys; use function array_map; -use function array_unique; use function count; use function get_class; use function in_array; @@ -854,7 +855,7 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc private function generateIntMaskType(Type $type): ?Type { - $ints = TypeUtils::getConstantIntegers($type); + $ints = array_map(static fn (ConstantIntegerType $type) => $type->getValue(), TypeUtils::getConstantIntegers($type)); if (count($ints) === 0) { return null; } @@ -862,18 +863,16 @@ private function generateIntMaskType(Type $type): ?Type $values = []; foreach ($ints as $int) { - $int = $int->getValue(); - - if ($int !== 0) { + if ($int !== 0 && !array_key_exists($int, $values)) { foreach ($values as $value) { - $values[] = $value | $int; + $values[$value | $int] = true; } } - $values[] = $int; + $values[$int] = true; } - $values = array_map(static fn ($value) => new ConstantIntegerType($value), array_unique($values)); + $values = array_map(static fn ($value) => new ConstantIntegerType($value), array_keys($values)); return TypeCombinator::union(...$values); } From d48e5594c48dadb22282fdd5885e224191a5d6b1 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Mon, 4 Apr 2022 09:29:12 +0200 Subject: [PATCH 5/5] Always add 0 and convert to range if continuous --- src/PhpDoc/TypeNodeResolver.php | 25 ++++++++++++++++-------- tests/PHPStan/Analyser/data/int-mask.php | 17 +++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) 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); } /**