Skip to content

Commit

Permalink
Add support for int-mask and int-mask-of types
Browse files Browse the repository at this point in the history
  • Loading branch information
rvanvelzen committed Apr 4, 2022
1 parent 2d2f653 commit 7f5f55b
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 0 deletions.
53 changes: 53 additions & 0 deletions src/PhpDoc/TypeNodeResolver.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Class::CONST*>
$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) {
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/Type/TypeUtils.php
Expand Up @@ -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;

Expand Down Expand Up @@ -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[]
*/
Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Expand Up @@ -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[]
Expand Down
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -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');
}

/**
Expand Down
23 changes: 23 additions & 0 deletions tests/PHPStan/Analyser/data/bug-4732.php
@@ -0,0 +1,23 @@
<?php declare(strict_types = 1);

namespace Bug4732;

class HelloWorld
{
const FOO_BAR = 1;
const FOO_BAZ = 2;

/**
* @param int-mask-of<self::FOO_*> $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);
}
}
43 changes: 43 additions & 0 deletions tests/PHPStan/Analyser/data/int-mask.php
@@ -0,0 +1,43 @@
<?php declare(strict_types = 1);

namespace IntMask;

use function PHPStan\Testing\assertType;

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 = '';

/**
* @param int-mask-of<self::FOO_*> $one
* @param int-mask<self::FOO_BAR, self::FOO_BAZ> $two
* @param int-mask<1, 2, 8> $three
* @param int-mask<1, 4, 16, 64, 256, 1024> $four
* @param int-mask-of<self::BAZ_*> $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<self::BAR_*> $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
}
}

0 comments on commit 7f5f55b

Please sign in to comment.