mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
Merge pull request #10150 from boesing/bugfix/enum-values
This commit is contained in:
commit
9b6edffc44
@ -4,7 +4,6 @@ namespace Psalm\Internal\Type\Comparator;
|
|||||||
|
|
||||||
use Psalm\Codebase;
|
use Psalm\Codebase;
|
||||||
use Psalm\Internal\MethodIdentifier;
|
use Psalm\Internal\MethodIdentifier;
|
||||||
use Psalm\Type;
|
|
||||||
use Psalm\Type\Atomic;
|
use Psalm\Type\Atomic;
|
||||||
use Psalm\Type\Atomic\Scalar;
|
use Psalm\Type\Atomic\Scalar;
|
||||||
use Psalm\Type\Atomic\TArray;
|
use Psalm\Type\Atomic\TArray;
|
||||||
@ -18,12 +17,10 @@ use Psalm\Type\Atomic\TConditional;
|
|||||||
use Psalm\Type\Atomic\TEmptyMixed;
|
use Psalm\Type\Atomic\TEmptyMixed;
|
||||||
use Psalm\Type\Atomic\TEnumCase;
|
use Psalm\Type\Atomic\TEnumCase;
|
||||||
use Psalm\Type\Atomic\TGenericObject;
|
use Psalm\Type\Atomic\TGenericObject;
|
||||||
use Psalm\Type\Atomic\TInt;
|
|
||||||
use Psalm\Type\Atomic\TIterable;
|
use Psalm\Type\Atomic\TIterable;
|
||||||
use Psalm\Type\Atomic\TKeyOf;
|
use Psalm\Type\Atomic\TKeyOf;
|
||||||
use Psalm\Type\Atomic\TKeyedArray;
|
use Psalm\Type\Atomic\TKeyedArray;
|
||||||
use Psalm\Type\Atomic\TList;
|
use Psalm\Type\Atomic\TList;
|
||||||
use Psalm\Type\Atomic\TLiteralInt;
|
|
||||||
use Psalm\Type\Atomic\TLiteralString;
|
use Psalm\Type\Atomic\TLiteralString;
|
||||||
use Psalm\Type\Atomic\TMixed;
|
use Psalm\Type\Atomic\TMixed;
|
||||||
use Psalm\Type\Atomic\TNamedObject;
|
use Psalm\Type\Atomic\TNamedObject;
|
||||||
@ -46,7 +43,6 @@ use function array_values;
|
|||||||
use function assert;
|
use function assert;
|
||||||
use function count;
|
use function count;
|
||||||
use function get_class;
|
use function get_class;
|
||||||
use function is_int;
|
|
||||||
use function strtolower;
|
use function strtolower;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -635,40 +631,6 @@ class AtomicTypeComparator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($input_type_part instanceof TEnumCase
|
|
||||||
&& $codebase->classlike_storage_provider->has($input_type_part->value)
|
|
||||||
) {
|
|
||||||
if ($container_type_part instanceof TString || $container_type_part instanceof TInt) {
|
|
||||||
$input_type_classlike_storage = $codebase->classlike_storage_provider->get($input_type_part->value);
|
|
||||||
if ($input_type_classlike_storage->enum_type === null
|
|
||||||
|| !isset($input_type_classlike_storage->enum_cases[$input_type_part->case_name])
|
|
||||||
) {
|
|
||||||
// Not a backed enum or non-existent enum case
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$input_type_enum_case_storage = $input_type_classlike_storage->enum_cases[$input_type_part->case_name];
|
|
||||||
assert(
|
|
||||||
$input_type_enum_case_storage->value !== null,
|
|
||||||
'Backed enums cannot have values without a value.',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (is_int($input_type_enum_case_storage->value)) {
|
|
||||||
return self::isContainedBy(
|
|
||||||
$codebase,
|
|
||||||
new TLiteralInt($input_type_enum_case_storage->value),
|
|
||||||
$container_type_part,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::isContainedBy(
|
|
||||||
$codebase,
|
|
||||||
Type::getAtomicStringFromLiteral($input_type_enum_case_storage->value),
|
|
||||||
$container_type_part,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($container_type_part instanceof TString || $container_type_part instanceof TScalar) {
|
if ($container_type_part instanceof TString || $container_type_part instanceof TScalar) {
|
||||||
if ($input_type_part instanceof TNamedObject) {
|
if ($input_type_part instanceof TNamedObject) {
|
||||||
// check whether the object has a __toString method
|
// check whether the object has a __toString method
|
||||||
|
@ -83,6 +83,7 @@ use function assert;
|
|||||||
use function count;
|
use function count;
|
||||||
use function explode;
|
use function explode;
|
||||||
use function get_class;
|
use function get_class;
|
||||||
|
use function in_array;
|
||||||
use function is_int;
|
use function is_int;
|
||||||
use function min;
|
use function min;
|
||||||
use function strlen;
|
use function strlen;
|
||||||
@ -533,7 +534,11 @@ class SimpleAssertionReconciler extends Reconciler
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($assertion_type instanceof TValueOf) {
|
if ($assertion_type instanceof TValueOf) {
|
||||||
return $assertion_type->type;
|
return self::reconcileValueOf(
|
||||||
|
$codebase,
|
||||||
|
$assertion_type,
|
||||||
|
$failed_reconciliation,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -2951,6 +2956,71 @@ class SimpleAssertionReconciler extends Reconciler
|
|||||||
return TypeCombiner::combine(array_values($matched_class_constant_types), $codebase);
|
return TypeCombiner::combine(array_values($matched_class_constant_types), $codebase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Reconciler::RECONCILIATION_* $failed_reconciliation
|
||||||
|
*/
|
||||||
|
private static function reconcileValueOf(
|
||||||
|
Codebase $codebase,
|
||||||
|
TValueOf $assertion_type,
|
||||||
|
int &$failed_reconciliation
|
||||||
|
): ?Union {
|
||||||
|
$reconciled_types = [];
|
||||||
|
|
||||||
|
// For now, only enums are supported here
|
||||||
|
foreach ($assertion_type->type->getAtomicTypes() as $atomic_type) {
|
||||||
|
$enum_case_to_assert = null;
|
||||||
|
if ($atomic_type instanceof TClassConstant) {
|
||||||
|
$class_name = $atomic_type->fq_classlike_name;
|
||||||
|
$enum_case_to_assert = $atomic_type->const_name;
|
||||||
|
} elseif ($atomic_type instanceof TNamedObject) {
|
||||||
|
$class_name = $atomic_type->value;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$codebase->classOrInterfaceOrEnumExists($class_name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$class_storage = $codebase->classlike_storage_provider->get($class_name);
|
||||||
|
if (!$class_storage->is_enum) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($class_storage->enum_type, ['string', 'int'], true)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For value-of<MyBackedEnum>, the assertion is meant to return *ANY* value of *ANY* enum case
|
||||||
|
if ($enum_case_to_assert === null) {
|
||||||
|
foreach ($class_storage->enum_cases as $enum_case) {
|
||||||
|
assert(
|
||||||
|
$enum_case->value !== null,
|
||||||
|
'Verified enum type above, value can not contain `null` anymore.',
|
||||||
|
);
|
||||||
|
$reconciled_types[] = Type::getLiteral($enum_case->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$enum_case = $class_storage->enum_cases[$atomic_type->const_name] ?? null;
|
||||||
|
if ($enum_case === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert($enum_case->value !== null, 'Verified enum type above, value can not contain `null` anymore.');
|
||||||
|
$reconciled_types[] = Type::getLiteral($enum_case->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($reconciled_types === []) {
|
||||||
|
$failed_reconciliation = Reconciler::RECONCILIATION_EMPTY;
|
||||||
|
return Type::getNever();
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeCombiner::combine($reconciled_types, $codebase, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @psalm-assert-if-true TCallableObject|TObjectWithProperties|TNamedObject $type
|
* @psalm-assert-if-true TCallableObject|TObjectWithProperties|TNamedObject $type
|
||||||
*/
|
*/
|
||||||
|
@ -58,6 +58,7 @@ use function array_values;
|
|||||||
use function explode;
|
use function explode;
|
||||||
use function get_class;
|
use function get_class;
|
||||||
use function implode;
|
use function implode;
|
||||||
|
use function is_int;
|
||||||
use function preg_quote;
|
use function preg_quote;
|
||||||
use function preg_replace;
|
use function preg_replace;
|
||||||
use function stripos;
|
use function stripos;
|
||||||
@ -258,6 +259,19 @@ abstract class Type
|
|||||||
return new Union([$type]);
|
return new Union([$type]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int|string $value
|
||||||
|
* @return TLiteralString|TLiteralInt
|
||||||
|
*/
|
||||||
|
public static function getLiteral($value): Atomic
|
||||||
|
{
|
||||||
|
if (is_int($value)) {
|
||||||
|
return new TLiteralInt($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TLiteralString::make($value);
|
||||||
|
}
|
||||||
|
|
||||||
public static function getString(?string $value = null): Union
|
public static function getString(?string $value = null): Union
|
||||||
{
|
{
|
||||||
return new Union([$value === null ? new TString() : self::getAtomicStringFromLiteral($value)]);
|
return new Union([$value === null ? new TString() : self::getAtomicStringFromLiteral($value)]);
|
||||||
|
@ -2194,6 +2194,10 @@ class AssertAnnotationTest extends TestCase
|
|||||||
function assertSomeInt(int $foo): void
|
function assertSomeInt(int $foo): void
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
/** @psalm-assert value-of<StringEnum|IntEnum> $foo */
|
||||||
|
function assertAnyEnumValue(string|int $foo): void
|
||||||
|
{}
|
||||||
|
|
||||||
/** @param "foo"|"bar" $foo */
|
/** @param "foo"|"bar" $foo */
|
||||||
function takesSomeStringFromEnum(string $foo): StringEnum
|
function takesSomeStringFromEnum(string $foo): StringEnum
|
||||||
{
|
{
|
||||||
@ -2216,8 +2220,14 @@ class AssertAnnotationTest extends TestCase
|
|||||||
|
|
||||||
assertSomeInt($int);
|
assertSomeInt($int);
|
||||||
takesSomeIntFromEnum($int);
|
takesSomeIntFromEnum($int);
|
||||||
|
|
||||||
|
/** @var string|int $potentialEnumValue */
|
||||||
|
$potentialEnumValue = null;
|
||||||
|
assertAnyEnumValue($potentialEnumValue);
|
||||||
',
|
',
|
||||||
'assertions' => [],
|
'assertions' => [
|
||||||
|
'$potentialEnumValue===' => "'bar'|'baz'|'foo'|1|2|3",
|
||||||
|
],
|
||||||
'ignored_issues' => [],
|
'ignored_issues' => [],
|
||||||
'php_version' => '8.1',
|
'php_version' => '8.1',
|
||||||
],
|
],
|
||||||
|
@ -1015,6 +1015,21 @@ class EnumTest extends TestCase
|
|||||||
'ignored_issues' => [],
|
'ignored_issues' => [],
|
||||||
'php_version' => '8.1',
|
'php_version' => '8.1',
|
||||||
],
|
],
|
||||||
|
'backedEnumDoesNotPassNativeType' => [
|
||||||
|
'code' => '<?php
|
||||||
|
enum State: string
|
||||||
|
{
|
||||||
|
case A = "A";
|
||||||
|
case B = "B";
|
||||||
|
case C = "C";
|
||||||
|
}
|
||||||
|
function f(string $state): void {}
|
||||||
|
f(State::A);
|
||||||
|
',
|
||||||
|
'error_message' => 'InvalidArgument',
|
||||||
|
'ignored_issues' => [],
|
||||||
|
'php_version' => '8.1',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user