Skip to content

Commit

Permalink
feature: introducing proper handling of value-of in combination wit…
Browse files Browse the repository at this point in the history
…h backed enums

This introduces both:
- a bugfix for a regression introduced by `31eaf83c4` which prevents backed enums are incorrectly identified as literals
- an additional feature so that `value-of` can be used with backed enums to assert any of the enum cases values

Signed-off-by: Maximilian Bösing <[email protected]>
  • Loading branch information
boesing committed Aug 25, 2023
1 parent 31eaf83 commit 5948559
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 2 deletions.
58 changes: 57 additions & 1 deletion src/Psalm/Internal/Type/SimpleAssertionReconciler.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
use function count;
use function explode;
use function get_class;
use function in_array;
use function is_int;
use function min;
use function strlen;
Expand Down Expand Up @@ -533,7 +534,10 @@ public static function reconcile(
}

if ($assertion_type instanceof TValueOf) {
return $assertion_type->type;
return self::reconcileValueOf(
$codebase,
$assertion_type
);
}

return null;
Expand Down Expand Up @@ -2951,6 +2955,58 @@ private static function reconcileClassConstant(
return TypeCombiner::combine(array_values($matched_class_constant_types), $codebase);
}

private static function reconcileValueOf(
Codebase $codebase,
TValueOf $assertion_type
): ?Union {
$reconciled_types = [];

// For now, only enums are supported here
foreach ($assertion_type->type->getAtomicTypes() as $atomic_type) {
$class_name = null;
$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) {
$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;
}

$reconciled_types[] = Type::getLiteral($enum_case->value);
}

return new Union($reconciled_types);
}

/**
* @psalm-assert-if-true TCallableObject|TObjectWithProperties|TNamedObject $type
*/
Expand Down
14 changes: 14 additions & 0 deletions src/Psalm/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
use function explode;
use function get_class;
use function implode;
use function is_int;
use function preg_quote;
use function preg_replace;
use function stripos;
Expand Down Expand Up @@ -258,6 +259,19 @@ public static function getNumericString(): Union
return new Union([$type]);
}

/**
* @param int|string $value
* @return TLiteralString|TLiteralInt
*/
public static function getLiteral($value)
{
if (is_int($value)) {
return new TLiteralInt($value);
}

return new TLiteralString($value);
}

public static function getString(?string $value = null): Union
{
return new Union([$value === null ? new TString() : self::getAtomicStringFromLiteral($value)]);
Expand Down
12 changes: 11 additions & 1 deletion tests/AssertAnnotationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2194,6 +2194,10 @@ function assertSomeString(string $foo): void
function assertSomeInt(int $foo): void
{}
/** @psalm-assert value-of<StringEnum|IntEnum> $foo */
function assertAnyEnumValue(string|int $foo): void
{}
/** @param "foo"|"bar" $foo */
function takesSomeStringFromEnum(string $foo): StringEnum
{
Expand All @@ -2216,8 +2220,14 @@ function takesSomeIntFromEnum(int $foo): IntEnum
assertSomeInt($int);
takesSomeIntFromEnum($int);
/** @var string|int $potentialEnumValue */
$potentialEnumValue = null;
assertAnyEnumValue($potentialEnumValue);
',
'assertions' => [],
'assertions' => [
'$potentialEnumValue===' => "'bar'|'baz'|'foo'|1|2|3",
],
'ignored_issues' => [],
'php_version' => '8.1',
],
Expand Down

0 comments on commit 5948559

Please sign in to comment.