mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Add support for int-mask<...> and int-mask-of<...>
This commit is contained in:
parent
597b58d3a4
commit
98b755fb6c
@ -360,6 +360,35 @@ class TypeExpander
|
||||
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
|
||||
|| $return_type instanceof Type\Atomic\TGenericObject
|
||||
|| $return_type instanceof Type\Atomic\TIterable
|
||||
|
@ -46,6 +46,10 @@ use function strlen;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
use function reset;
|
||||
use function is_int;
|
||||
use function array_merge;
|
||||
use function array_unique;
|
||||
|
||||
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])
|
||||
&& $generic_type_value !== 'self'
|
||||
&& $generic_type_value !== 'static'
|
||||
@ -1027,4 +1116,36 @@ class TypeParser
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,8 @@ class TypeTokenizer
|
||||
'closed-resource' => true,
|
||||
'associative-array' => true,
|
||||
'arraylike-object' => true,
|
||||
'int-mask' => true,
|
||||
'int-mask-of' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
|
73
src/Psalm/Type/Atomic/TIntMask.php
Normal file
73
src/Psalm/Type/Atomic/TIntMask.php
Normal 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;
|
||||
}
|
||||
}
|
63
src/Psalm/Type/Atomic/TIntMaskOf.php
Normal file
63
src/Psalm/Type/Atomic/TIntMaskOf.php
Normal 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;
|
||||
}
|
||||
}
|
@ -543,7 +543,7 @@ function explode(string $delimiter, string $string, int $limit = -1) : array {}
|
||||
*
|
||||
* @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
|
||||
*
|
||||
|
@ -898,6 +898,60 @@ class TypeParseTest extends TestCase
|
||||
$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
|
||||
{
|
||||
if (!function_exists('Psalm\Tests\someFunction')) {
|
||||
|
Loading…
Reference in New Issue
Block a user