1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Add support for int-mask<...> and int-mask-of<...>

This commit is contained in:
Matt Brown 2020-10-30 13:22:43 -04:00 committed by Daniil Gentili
parent 597b58d3a4
commit 98b755fb6c
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
7 changed files with 343 additions and 1 deletions

View File

@ -360,6 +360,35 @@ class TypeExpander
return $return_type; return $return_type;
} }
if ($return_type instanceof Type\Atomic\TIntMask) {
if (!$evaluate_class_constants) {
return new Type\Atomic\TInt();
}
$potential_ints = [];
foreach ($return_type->values as $value_type) {
$new_value_type = self::expandAtomic(
$codebase,
$value_type,
$self_class,
$static_class_type,
$parent_class,
$evaluate_class_constants,
$evaluate_conditional_types,
$final
);
if (\is_array($new_value_type) || !$new_value_type instanceof Type\Atomic\TLiteralInt) {
return new Type\Atomic\TInt();
}
$potential_ints[] = $new_value_type->value;
}
return \Psalm\Internal\Type\TypeParser::getComputedIntsFromMask($potential_ints);
}
if ($return_type instanceof Type\Atomic\TArray if ($return_type instanceof Type\Atomic\TArray
|| $return_type instanceof Type\Atomic\TGenericObject || $return_type instanceof Type\Atomic\TGenericObject
|| $return_type instanceof Type\Atomic\TIterable || $return_type instanceof Type\Atomic\TIterable

View File

@ -46,6 +46,10 @@ use function strlen;
use function strpos; use function strpos;
use function strtolower; use function strtolower;
use function substr; use function substr;
use function reset;
use function is_int;
use function array_merge;
use function array_unique;
class TypeParser class TypeParser
{ {
@ -348,6 +352,91 @@ class TypeParser
); );
} }
if ($generic_type_value === 'int-mask') {
$atomic_types = [];
foreach ($generic_params as $generic_param) {
if (!$generic_param->isSingle()) {
throw new TypeParseTreeException(
'int-mask types must all be non-union'
);
}
$generic_param_atomics = $generic_param->getAtomicTypes();
$atomic_type = reset($generic_param_atomics);
if ($atomic_type instanceof TNamedObject) {
if (\defined($atomic_type->value)) {
/** @var mixed */
$constant_value = \constant($atomic_type->value);
if (!is_int($constant_value)) {
throw new TypeParseTreeException(
'int-mask types must all be integer values'
);
}
$atomic_type = new TLiteralInt($constant_value);
} else {
throw new TypeParseTreeException(
'int-mask types must all be integer values'
);
}
}
if (!$atomic_type instanceof TLiteralInt
&& !($atomic_type instanceof Atomic\TScalarClassConstant
&& strpos($atomic_type->const_name, '*') === false)
) {
throw new TypeParseTreeException(
'int-mask types must all be integer values or scalar class constants'
);
}
$atomic_types[] = $atomic_type;
}
$potential_ints = [];
foreach ($atomic_types as $atomic_type) {
if (!$atomic_type instanceof TLiteralInt) {
return new Atomic\TIntMask($atomic_types);
}
$potential_ints[] = $atomic_type->value;
}
return new Union(self::getComputedIntsFromMask($potential_ints));
}
if ($generic_type_value === 'int-mask-of') {
$param_union_types = array_values($generic_params[0]->getAtomicTypes());
if (count($param_union_types) > 1) {
throw new TypeParseTreeException('Union types are not allowed in value-of type');
}
$param_type = $param_union_types[0];
if (!$param_type instanceof Atomic\TScalarClassConstant
&& !$param_type instanceof Atomic\TValueOfClassConstant
&& !$param_type instanceof Atomic\TKeyOfClassConstant
) {
throw new TypeParseTreeException(
'Invalid reference passed to int-mask-of'
);
} elseif ($param_type instanceof Atomic\TScalarClassConstant
&& strpos($param_type->const_name, '*') === false
) {
throw new TypeParseTreeException(
'Class constant passed to int-mask-of must be a wildcard type'
);
}
return new Atomic\TIntMaskOf($param_type);
}
if (isset(TypeTokenizer::PSALM_RESERVED_WORDS[$generic_type_value]) if (isset(TypeTokenizer::PSALM_RESERVED_WORDS[$generic_type_value])
&& $generic_type_value !== 'self' && $generic_type_value !== 'self'
&& $generic_type_value !== 'static' && $generic_type_value !== 'static'
@ -1027,4 +1116,36 @@ class TypeParser
throw new \LogicException('Should never get here'); throw new \LogicException('Should never get here');
} }
/**
* @param non-empty-list<int> $potential_ints
* @return non-empty-list<TLiteralInt>
*/
public static function getComputedIntsFromMask(array $potential_ints) : array
{
$potential_values = [];
foreach ($potential_ints as $ith) {
$new_values = [];
$new_values[] = $ith;
if ($ith !== 0) {
for ($j = 0; $j < count($potential_values); $j++) {
$new_values[] = $ith | $potential_values[$j];
}
}
$potential_values = array_merge($new_values, $potential_values);
}
$potential_values = array_unique($potential_values);
return array_map(
function ($int) {
return new TLiteralInt($int);
},
array_values($potential_values)
);
}
} }

View File

@ -73,6 +73,8 @@ class TypeTokenizer
'closed-resource' => true, 'closed-resource' => true,
'associative-array' => true, 'associative-array' => true,
'arraylike-object' => true, 'arraylike-object' => true,
'int-mask' => true,
'int-mask-of' => true,
]; ];
/** /**

View File

@ -0,0 +1,73 @@
<?php
namespace Psalm\Type\Atomic;
use function substr;
class TIntMask extends TInt
{
/** @var non-empty-array<TLiteralInt|TScalarClassConstant> */
public $values;
/** @param non-empty-array<TLiteralInt|TScalarClassConstant> $values */
public function __construct(array $values)
{
$this->values = $values;
}
public function getKey(bool $include_extra = true): string
{
$s = '';
foreach ($this->values as $value) {
$s .= $value->getKey() . ', ';
}
return 'int-mask<' . substr($s, 0, -2) . '>';
}
public function getId(bool $nested = false): string
{
return $this->getKey();
}
/**
* @param array<string> $aliased_classes
*/
public function toPhpString(
?string $namespace,
array $aliased_classes,
?string $this_class,
int $php_major_version,
int $php_minor_version
): ?string {
return $php_major_version >= 7 ? 'int' : null;
}
/**
* @param array<string, string> $aliased_classes
*
*/
public function toNamespacedString(
?string $namespace,
array $aliased_classes,
?string $this_class,
bool $use_phpdoc_format
): string {
if ($use_phpdoc_format) {
return 'int';
}
$s = '';
foreach ($this->values as $value) {
$s .= $value->toNamespacedString($namespace, $aliased_classes, $this_class, $use_phpdoc_format) . ', ';
}
return 'int-mask<' . substr($s, 0, -2) . '>';
}
public function canBeFullyExpressedInPhp(): bool
{
return false;
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Psalm\Type\Atomic;
class TIntMaskOf extends TInt
{
/** @var TScalarClassConstant|TKeyOfClassConstant|TValueOfClassConstant */
public $value;
/**
* @param TScalarClassConstant|TKeyOfClassConstant|TValueOfClassConstant $value
*/
public function __construct(\Psalm\Type\Atomic $value)
{
$this->value = $value;
}
public function getKey(bool $include_extra = true): string
{
return 'int-mask-of<' . $this->value->getKey() . '>';
}
public function getId(bool $nested = false): string
{
return $this->getKey();
}
/**
* @param array<string> $aliased_classes
*/
public function toPhpString(
?string $namespace,
array $aliased_classes,
?string $this_class,
int $php_major_version,
int $php_minor_version
): ?string {
return $php_major_version >= 7 ? 'int' : null;
}
/**
* @param array<string, string> $aliased_classes
*
*/
public function toNamespacedString(
?string $namespace,
array $aliased_classes,
?string $this_class,
bool $use_phpdoc_format
): string {
if ($use_phpdoc_format) {
return 'int';
}
return 'int-mask-of<'
. $this->value->toNamespacedString($namespace, $aliased_classes, $this_class, false)
. '>';
}
public function canBeFullyExpressedInPhp(): bool
{
return false;
}
}

View File

@ -543,7 +543,7 @@ function explode(string $delimiter, string $string, int $limit = -1) : array {}
* *
* @psalm-flow ($subject) -(array-assignment)-> return * @psalm-flow ($subject) -(array-assignment)-> return
* *
* @template TFlags as 0|1|2|3|4|5|6|7 * @template TFlags as int-mask<0, 1, 2, 4>
* *
* @param TFlags $flags * @param TFlags $flags
* *

View File

@ -898,6 +898,60 @@ class TypeParseTest extends TestCase
$this->assertSame($resolved_type->getId(), $docblock_type->getId()); $this->assertSame($resolved_type->getId(), $docblock_type->getId());
} }
public function testIntMaskWithInts(): void
{
$docblock_type = Type::parseString('int-mask<0, 1, 2, 4>');
$this->assertSame('int(0)|int(1)|int(2)|int(3)|int(4)|int(5)|int(6)|int(7)', $docblock_type->getId());
$docblock_type = Type::parseString('int-mask<1, 2, 4>');
$this->assertSame('int(1)|int(2)|int(3)|int(4)|int(5)|int(6)|int(7)', $docblock_type->getId());
$docblock_type = Type::parseString('int-mask<1, 4>');
$this->assertSame('int(1)|int(4)|int(5)', $docblock_type->getId());
$docblock_type = Type::parseString('int-mask<PREG_PATTERN_ORDER, PREG_OFFSET_CAPTURE, PREG_UNMATCHED_AS_NULL>');
$this->assertSame('int(1)|int(256)|int(257)|int(512)|int(513)|int(768)|int(769)', $docblock_type->getId());
}
public function testIntMaskWithClassConstant(): void
{
$docblock_type = Type::parseString('int-mask<0, A::FOO, A::BAR>');
$this->assertSame('int-mask<int(0), scalar-class-constant(A::FOO), scalar-class-constant(A::BAR)>', $docblock_type->getId());
}
public function testIntMaskWithInvalidClassConstant(): void
{
$this->expectException(\Psalm\Exception\TypeParseTreeException::class);
Type::parseString('int-mask<A::*>');
}
public function testIntMaskOfWithValidClassConstant(): void
{
$docblock_type = Type::parseString('int-mask-of<A::*>');
$this->assertSame('int-mask-of<scalar-class-constant(A::*)>', $docblock_type->getId());
}
public function testIntMaskOfWithInvalidClassConstant(): void
{
$this->expectException(\Psalm\Exception\TypeParseTreeException::class);
Type::parseString('int-mask-of<A::FOO>');
}
public function testIntMaskOfWithValidValueOf(): void
{
$docblock_type = Type::parseString('int-mask-of<value-of<A::FOO>>');
$this->assertSame('int-mask-of<value-of<A::FOO>>', $docblock_type->getId());
}
public function testReflectionTypeParse(): void public function testReflectionTypeParse(): void
{ {
if (!function_exists('Psalm\Tests\someFunction')) { if (!function_exists('Psalm\Tests\someFunction')) {