1
0
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:
Bruce Weirdan 2021-11-28 09:37:57 +02:00
parent 2df5f22713
commit 806db287d2
No known key found for this signature in database
GPG Key ID: CFC3AAB181751B0D
5 changed files with 185 additions and 16 deletions

View File

@ -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) {

View File

@ -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

View File

@ -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');

View File

@ -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 {

View File

@ -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',
],
];
}