Skip to content

Commit

Permalink
Fix get_object_vars on enums
Browse files Browse the repository at this point in the history
  • Loading branch information
jack-worman committed Dec 21, 2022
1 parent c75f06e commit 472e201
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 4 deletions.
37 changes: 34 additions & 3 deletions src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
Expand Up @@ -65,6 +65,8 @@
use RuntimeException;
use UnexpectedValueException;

use function array_keys;
use function array_map;
use function array_merge;
use function array_pop;
use function array_shift;
Expand All @@ -73,9 +75,12 @@
use function count;
use function get_class;
use function implode;
use function is_int;
use function is_string;
use function preg_match;
use function preg_replace;
use function preg_split;
use function sprintf;
use function str_replace;
use function strtolower;
use function trim;
Expand Down Expand Up @@ -1415,6 +1420,34 @@ private function visitEnumDeclaration(
),
);
}

$storage->declaring_property_ids['name'] = $storage->name;
$storage->appearing_property_ids['name'] = "{$storage->name}::\$name";
$storage->properties['name'] = new PropertyStorage();
$storage->properties['name']->type = new Union(array_map(
fn(string $name): Type\Atomic\TLiteralString => new Type\Atomic\TLiteralString($name),
array_keys($storage->enum_cases),
));
if ($storage->enum_type !== null) {
$storage->declaring_property_ids['value'] = $storage->name;
$storage->appearing_property_ids['value'] = "{$storage->name}::\$value";
$valueTypes = [];
foreach ($storage->enum_cases as $enumCaseStorage) {
if (is_string($enumCaseStorage->value)) {
$valueTypes[] = new Type\Atomic\TLiteralString($enumCaseStorage->value);
} elseif (is_int($enumCaseStorage->value)) {
$valueTypes[] = new Type\Atomic\TLiteralInt($enumCaseStorage->value);
} else {
throw new LogicException(sprintf(
'%s has a null value, but the %s::$enum_type is not null.',
EnumCaseStorage::class,
ClassLikeStorage::class,
));
}
}
$storage->properties['value'] = new PropertyStorage();
$storage->properties['value']->type = new Union($valueTypes);
}
}

/**
Expand Down Expand Up @@ -1580,9 +1613,7 @@ private function visitPropertyDeclaration(
}

if ($doc_var_group_type) {
$property_storage->type = count($stmt->props) === 1
? $doc_var_group_type
: $doc_var_group_type;
$property_storage->type = $doc_var_group_type;
}
}

Expand Down
Expand Up @@ -2,6 +2,8 @@

namespace Psalm\Internal\Provider\ReturnTypeProvider;

use BackedEnum;
use LogicException;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\ClassAnalyzer;
Expand All @@ -10,6 +12,8 @@
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\EnumCaseStorage;
use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TArray;
Expand All @@ -18,9 +22,13 @@
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Union;
use UnitEnum;
use stdClass;

use function is_int;
use function is_string;
use function reset;
use function sprintf;
use function strtolower;

/**
Expand Down Expand Up @@ -55,6 +63,28 @@ public static function getGetObjectVarsReturnType(
$atomics = $first_arg_type->getAtomicTypes();
$object_type = reset($atomics);

if ($object_type instanceof Atomic\TEnumCase) {
$properties = ['name' => new Union([new Atomic\TLiteralString($object_type->case_name)])];
$codebase = $statements_source->getCodebase();
$enum_classlike_storage = $codebase->classlike_storage_provider->get($object_type->value);
if ($enum_classlike_storage->enum_type === null) {
return new TKeyedArray($properties);
}
$enum_case_storage = $enum_classlike_storage->enum_cases[$object_type->case_name];
if (is_int($enum_case_storage->value)) {
$properties['value'] = new Union([new Atomic\TLiteralInt($enum_case_storage->value)]);
} elseif (is_string($enum_case_storage->value)) {
$properties['value'] = new Union([new Atomic\TLiteralString($enum_case_storage->value)]);
} else {
throw new LogicException(sprintf(
'%s has a null value, but the %s::$enum_type is not null.',
EnumCaseStorage::class,
ClassLikeStorage::class,
));
}
return new TKeyedArray($properties);
}

if ($object_type instanceof TObjectWithProperties) {
if ([] === $object_type->properties) {
return self::$fallback;
Expand Down Expand Up @@ -126,7 +156,12 @@ public static function getGetObjectVarsReturnType(
return new TKeyedArray(
$properties,
null,
$class_storage->final ? null : [Type::getString(), Type::getMixed()],
$class_storage->final
|| $class_storage->name === UnitEnum::class
|| $class_storage->name === BackedEnum::class
|| $codebase->classImplements($class_storage->name, UnitEnum::class)
? null
: [Type::getString(), Type::getMixed()],
);
}
}
Expand Down
2 changes: 2 additions & 0 deletions stubs/Php81.phpstub
Expand Up @@ -13,6 +13,8 @@ namespace {

interface BackedEnum extends UnitEnum
{
/** @var non-empty-string $name */
public readonly string $name;
public readonly int|string $value;

/**
Expand Down
104 changes: 104 additions & 0 deletions tests/ReturnTypeProvider/GetObjectVarsTest.php
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Psalm\Tests\ReturnTypeProvider;

use Psalm\Tests\TestCase;
Expand Down Expand Up @@ -168,5 +170,107 @@ public function __construct(public string $t) {}
],
'php_version' => '8.2',
];

yield 'UnitEnum generic' => [
'code' => <<<'PHP'
<?php
enum A { case One; case Two; }
function getUnitEnum(): UnitEnum { return A::One; }
$b = get_object_vars(getUnitEnum());
PHP,
'assertions' => [
'$b===' => 'array{name: non-empty-string}',
],
'ignored_issues' => [],
'php_version' => '8.1',
];
yield 'UnitEnum specific' => [
'code' => <<<'PHP'
<?php
enum A { case One; case Two; }
function getUnitEnum(): A { return A::One; }
$b = get_object_vars(getUnitEnum());
PHP,
'assertions' => [
'$b===' => "array{name: 'One'|'Two'}",
],
'ignored_issues' => [],
'php_version' => '8.1',
];
yield 'UnitEnum literal' => [
'code' => <<<'PHP'
<?php
enum A { case One; case Two; }
$b = get_object_vars(A::One);
PHP,
'assertions' => [
'$b===' => "array{name: 'One'}",
],
'ignored_issues' => [],
'php_version' => '8.1',
];
yield 'BackedEnum generic' => [
'code' => <<<'PHP'
<?php
enum A: int { case One = 1; case Two = 2; }
function getBackedEnum(): BackedEnum { return A::One; }
$b = get_object_vars(getBackedEnum());
PHP,
'assertions' => [
'$b===' => 'array{name: non-empty-string, value: int|string}',
],
'ignored_issues' => [],
'php_version' => '8.1',
];
yield 'Int BackedEnum specific' => [
'code' => <<<'PHP'
<?php
enum A: int { case One = 1; case Two = 2; }
function getBackedEnum(): A { return A::One; }
$b = get_object_vars(getBackedEnum());
PHP,
'assertions' => [
'$b===' => "array{name: 'One'|'Two', value: 1|2}",
],
'ignored_issues' => [],
'php_version' => '8.1',
];
yield 'String BackedEnum specific' => [
'code' => <<<'PHP'
<?php
enum A: string { case One = "one"; case Two = "two"; }
function getBackedEnum(): A { return A::One; }
$b = get_object_vars(getBackedEnum());
PHP,
'assertions' => [
'$b===' => "array{name: 'One'|'Two', value: 'one'|'two'}",
],
'ignored_issues' => [],
'php_version' => '8.1',
];
yield 'Int BackedEnum literal' => [
'code' => <<<'PHP'
<?php
enum A: int { case One = 1; case Two = 2; }
$b = get_object_vars(A::One);
PHP,
'assertions' => [
'$b===' => "array{name: 'One', value: 1}",
],
'ignored_issues' => [],
'php_version' => '8.1',
];
yield 'String BackedEnum literal' => [
'code' => <<<'PHP'
<?php
enum A: string { case One = "one"; case Two = "two"; }
$b = get_object_vars(A::One);
PHP,
'assertions' => [
'$b===' => "array{name: 'One', value: 'one'}",
],
'ignored_issues' => [],
'php_version' => '8.1',
];
}
}

0 comments on commit 472e201

Please sign in to comment.