mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Infer ::from()
and ::tryFrom()
return types on backed enums
Fixes vimeo/psalm#6429
This commit is contained in:
parent
2df5f22713
commit
806db287d2
@ -2354,8 +2354,15 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
if ($storage->is_enum && $interface_method_name_lc === 'cases') {
|
||||
continue;
|
||||
if ($storage->is_enum) {
|
||||
if ($interface_method_name_lc === 'cases') {
|
||||
continue;
|
||||
}
|
||||
if ($storage->enum_type
|
||||
&& in_array($interface_method_name_lc, ['from', 'tryfrom'], true)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$implementer_method_storage) {
|
||||
|
@ -121,8 +121,16 @@ class Methods
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($class_storage->is_enum && $method_name === 'cases') {
|
||||
return true;
|
||||
if ($class_storage->is_enum) {
|
||||
if ($method_name === 'cases') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($class_storage->enum_type
|
||||
&& \in_array($method_name, ['from', 'tryFrom'], true)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$source_file_path = $source ? $source->getFilePath() : $source_file_path;
|
||||
@ -686,21 +694,55 @@ class Methods
|
||||
$appearing_fq_class_storage = $this->classlike_storage_provider->get($appearing_fq_class_name);
|
||||
|
||||
if ($appearing_fq_class_name === 'UnitEnum'
|
||||
&& $original_method_name === 'cases'
|
||||
&& $original_class_storage->is_enum
|
||||
&& $original_class_storage->enum_cases
|
||||
) {
|
||||
$types = [];
|
||||
if ($original_method_name === 'cases') {
|
||||
if ($original_class_storage->enum_cases === []) {
|
||||
return Type::getEmptyArray();
|
||||
}
|
||||
$types = [];
|
||||
|
||||
foreach ($original_class_storage->enum_cases as $case_name => $_) {
|
||||
$types[] = new Type\Union([new Type\Atomic\TEnumCase($original_fq_class_name, $case_name)]);
|
||||
foreach ($original_class_storage->enum_cases as $case_name => $_) {
|
||||
$types[] = new Type\Union([new Type\Atomic\TEnumCase($original_fq_class_name, $case_name)]);
|
||||
}
|
||||
|
||||
$list = new Type\Atomic\TKeyedArray($types);
|
||||
$list->is_list = true;
|
||||
$list->sealed = true;
|
||||
return new Type\Union([$list]);
|
||||
}
|
||||
}
|
||||
|
||||
$list = new Type\Atomic\TKeyedArray($types);
|
||||
$list->is_list = true;
|
||||
$list->sealed = true;
|
||||
|
||||
return new Type\Union([$list]);
|
||||
if ($appearing_fq_class_name === 'BackedEnum'
|
||||
&& $original_class_storage->is_enum
|
||||
&& $original_class_storage->enum_type
|
||||
) {
|
||||
if (($original_method_name === 'from'
|
||||
|| $original_method_name === 'tryfrom'
|
||||
) && $source_analyzer
|
||||
&& isset($args[0])
|
||||
&& ($first_arg_type = $source_analyzer->getNodeTypeProvider()->getType($args[0]->value))
|
||||
) {
|
||||
$types = [];
|
||||
foreach ($original_class_storage->enum_cases as $case_name => $case_storage) {
|
||||
if (UnionTypeComparator::isContainedBy(
|
||||
$source_analyzer->getCodebase(),
|
||||
\is_int($case_storage->value) ?
|
||||
Type::getInt(false, $case_storage->value) :
|
||||
Type::getString($case_storage->value),
|
||||
$first_arg_type
|
||||
)) {
|
||||
$types[] = new Type\Atomic\TEnumCase($original_fq_class_name, $case_name);
|
||||
}
|
||||
}
|
||||
if ($types) {
|
||||
if ($original_method_name === 'tryfrom') {
|
||||
$types[] = new Type\Atomic\TNull();
|
||||
}
|
||||
return new Type\Union($types);
|
||||
}
|
||||
return $original_method_name === 'tryfrom' ? Type::getNull() : Type::getNever();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$appearing_fq_class_storage->user_defined
|
||||
|
@ -300,7 +300,17 @@ class ClassLikeNodeScanner
|
||||
$this->file_storage->has_visitor_issues = true;
|
||||
$storage->has_visitor_issues = true;
|
||||
}
|
||||
// todo: $this->codebase->scanner->queueClassLikeForScanning('BackedEnum');
|
||||
$storage->class_implements['backedenum'] = 'BackedEnum';
|
||||
$storage->direct_class_interfaces['backedenum'] = 'BackedEnum';
|
||||
$this->file_storage->required_interfaces['backedenum'] = 'BackedEnum';
|
||||
$this->codebase->scanner->queueClassLikeForScanning('BackedEnum');
|
||||
$storage->declaring_method_ids['from'] = new \Psalm\Internal\MethodIdentifier('BackedEnum', 'from');
|
||||
$storage->appearing_method_ids['from'] = $storage->declaring_method_ids['from'];
|
||||
$storage->declaring_method_ids['tryfrom'] = new \Psalm\Internal\MethodIdentifier(
|
||||
'BackedEnum',
|
||||
'tryfrom'
|
||||
);
|
||||
$storage->appearing_method_ids['tryfrom'] = $storage->declaring_method_ids['tryfrom'];
|
||||
}
|
||||
|
||||
$this->codebase->scanner->queueClassLikeForScanning('UnitEnum');
|
||||
|
@ -3,10 +3,17 @@ namespace {
|
||||
interface UnitEnum {
|
||||
/** @var non-empty-string $name */
|
||||
public readonly string $name;
|
||||
|
||||
|
||||
/** @return non-empty-list<static> */
|
||||
public static function cases(): array;
|
||||
}
|
||||
|
||||
interface BackedEnum
|
||||
{
|
||||
public readonly int|string $value;
|
||||
public static function from(string|int $value): static;
|
||||
public static function tryFrom(string|int $value): ?static;
|
||||
}
|
||||
}
|
||||
|
||||
namespace FTP {
|
||||
|
@ -261,6 +261,109 @@ class EnumTest extends TestCase
|
||||
[],
|
||||
'8.1',
|
||||
],
|
||||
'casesOnEnumWithNoCasesReturnEmptyArray' => [
|
||||
'<?php
|
||||
enum Status: int {}
|
||||
$_z = Status::cases();
|
||||
',
|
||||
'assertions' => [
|
||||
'$_z===' => 'array<empty, empty>',
|
||||
],
|
||||
[],
|
||||
'8.1',
|
||||
],
|
||||
'backedEnumFromReturnsInstanceOfThatEnum' => [
|
||||
'<?php
|
||||
enum Status: int {
|
||||
case Open = 1;
|
||||
case Closed = 2;
|
||||
}
|
||||
|
||||
function f(): Status {
|
||||
return Status::from(1);
|
||||
}
|
||||
',
|
||||
'assertions' => [],
|
||||
[],
|
||||
'8.1',
|
||||
],
|
||||
'backedEnumTryFromReturnsInstanceOfThatEnum' => [
|
||||
'<?php
|
||||
enum Status: int {
|
||||
case Open = 1;
|
||||
case Closed = 2;
|
||||
}
|
||||
|
||||
function f(): Status {
|
||||
return Status::tryFrom(rand(1, 10)) ?? Status::Open;
|
||||
}
|
||||
',
|
||||
'assertions' => [],
|
||||
[],
|
||||
'8.1',
|
||||
],
|
||||
'backedEnumFromReturnsSpecificCase' => [
|
||||
'<?php
|
||||
enum Status: int {
|
||||
case Open = 1;
|
||||
case Closed = 2;
|
||||
}
|
||||
|
||||
$_z = Status::from(2);
|
||||
',
|
||||
'assertions' => [
|
||||
'$_z===' => 'enum(Status::Closed)',
|
||||
],
|
||||
[],
|
||||
'8.1',
|
||||
],
|
||||
'backedEnumTryFromReturnsSpecificCase' => [
|
||||
'<?php
|
||||
enum Status: int {
|
||||
case Open = 1;
|
||||
case Closed = 2;
|
||||
}
|
||||
|
||||
$_z = Status::tryFrom(2);
|
||||
',
|
||||
'assertions' => [
|
||||
'$_z===' => 'enum(Status::Closed)|null',
|
||||
],
|
||||
[],
|
||||
'8.1',
|
||||
],
|
||||
'backedEnumFromReturnsUnionOfCases' => [
|
||||
'<?php
|
||||
enum Status: int {
|
||||
case Open = 1;
|
||||
case Closed = 2;
|
||||
case Busted = 3;
|
||||
}
|
||||
|
||||
$_z = Status::from(rand(1, 2));
|
||||
',
|
||||
'assertions' => [
|
||||
'$_z===' => 'enum(Status::Closed)|enum(Status::Open)',
|
||||
],
|
||||
[],
|
||||
'8.1',
|
||||
],
|
||||
'backedEnumTryFromReturnsUnionOfCases' => [
|
||||
'<?php
|
||||
enum Status: int {
|
||||
case Open = 1;
|
||||
case Closed = 2;
|
||||
case Busted = 3;
|
||||
}
|
||||
|
||||
$_z = Status::tryFrom(rand(1, 2));
|
||||
',
|
||||
'assertions' => [
|
||||
'$_z===' => 'enum(Status::Closed)|enum(Status::Open)|null',
|
||||
],
|
||||
[],
|
||||
'8.1',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user