Skip to content

Commit

Permalink
Narrow ->value of enum case(s) to only the possible values
Browse files Browse the repository at this point in the history
Using $stmt_var_type to determine if we're dealing with a subset of the
enum cases. If so, we only consider those cases for the possible values.

Fixes #8568
Fixes #8812
  • Loading branch information
annervisser committed Jan 21, 2023
1 parent 9e34de5 commit d86fcf4
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 4 deletions.
Expand Up @@ -62,14 +62,18 @@
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;

use function array_filter;
use function array_keys;
use function array_map;
use function array_search;
use function count;
use function in_array;
use function is_int;
use function is_string;
use function strtolower;

use const ARRAY_FILTER_USE_KEY;

/**
* @internal
*/
Expand Down Expand Up @@ -198,7 +202,7 @@ public static function analyze(
]),
);
} elseif ($prop_name === 'value' && $class_storage->enum_type !== null && $class_storage->enum_cases) {
self::handleEnumValue($statements_analyzer, $stmt, $class_storage);
self::handleEnumValue($statements_analyzer, $stmt, $stmt_var_type, $class_storage);
} elseif ($prop_name === 'name') {
self::handleEnumName($statements_analyzer, $stmt, $lhs_type_part);
} else {
Expand Down Expand Up @@ -941,11 +945,31 @@ private static function handleEnumName(
private static function handleEnumValue(
StatementsAnalyzer $statements_analyzer,
PropertyFetch $stmt,
Union $stmt_var_type,
ClassLikeStorage $class_storage
): void {
$relevant_enum_cases = array_filter(
$stmt_var_type->getAtomicTypes(),
static fn(Atomic $type): bool => $type instanceof TEnumCase,
);
$relevant_enum_case_names = array_map(
static fn(TEnumCase $enumCase): string => $enumCase->case_name,
$relevant_enum_cases,
);

$enum_cases = $class_storage->enum_cases;
if (!empty($relevant_enum_case_names)) {
// If we have a known subset of enum cases, include only those
$enum_cases = array_filter(
$enum_cases,
static fn(string $key) => in_array($key, $relevant_enum_case_names, true),
ARRAY_FILTER_USE_KEY,
);
}

$case_values = [];

foreach ($class_storage->enum_cases as $enum_case) {
foreach ($enum_cases as $enum_case) {
if (is_string($enum_case->value)) {
$case_values[] = new TLiteralString($enum_case->value);
} elseif (is_int($enum_case->value)) {
Expand All @@ -956,7 +980,6 @@ private static function handleEnumValue(
}
}

// todo: this is suboptimal when we reference enum directly, e.g. Status::Open->value
/** @psalm-suppress ArgumentTypeCoercion */
$statements_analyzer->node_data->setType(
$stmt,
Expand Down
60 changes: 59 additions & 1 deletion tests/EnumTest.php
Expand Up @@ -94,12 +94,70 @@ enum Mask: int {
$z = Mask::Two->value;
',
'assertions' => [
// xxx: we should be able to do better when we reference a case explicitly, like above
'$z===' => '2',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
'EnumCaseValue #8568' => [
'code' => '<?php
enum Mask: int {
case One = 1 << 0;
case Two = 1 << 1;
}
/** @return Mask */
function a() {
return Mask::One;
}
$z = a()->value;
',
'assertions' => [
'$z===' => '1|2',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
'EnumUnionAsCaseValue #8568' => [
'code' => '<?php
enum Mask: int {
case One = 1 << 0;
case Two = 1 << 1;
case Four = 1 << 2;
}
/** @return Mask::One|Mask::Two */
function a() {
return Mask::One;
}
$z = a()->value;
',
'assertions' => [
'$z===' => '1|2',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
'matchCaseOnEnumValue #8812' => [
'code' => '<?php
enum SomeType: string
{
case FOO = "FOO";
case BAR = "BAR";
}
function getSomething(string $moduleString): int
{
return match ($moduleString) {
SomeType::FOO->value => 1,
SomeType::BAR->value => 2,
};
}
',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1',
],
'namePropertyFromOutside' => [
'code' => '<?php
enum Status
Expand Down

0 comments on commit d86fcf4

Please sign in to comment.