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 22, 2022
1 parent c75f06e commit baab078
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 10 deletions.
19 changes: 19 additions & 0 deletions src/Psalm/Internal/Codebase/Populator.php
Expand Up @@ -2,6 +2,7 @@

namespace Psalm\Internal\Codebase;

use BackedEnum;
use InvalidArgumentException;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\MethodIdentifier;
Expand All @@ -14,8 +15,13 @@
use Psalm\Storage\ClassConstantStorage;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\FileStorage;
use Psalm\Storage\PropertyStorage;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TNonEmptyString;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;
use UnitEnum;

use function array_filter;
use function array_intersect_key;
Expand Down Expand Up @@ -688,6 +694,19 @@ private function populateInterfaceDataFromParentInterface(
$parent_interface_storage->parent_interfaces,
$storage->parent_interfaces,
);

if (isset($storage->parent_interfaces[strtolower(UnitEnum::class)])) {
$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([new TNonEmptyString()]);
}
if (isset($storage->parent_interfaces[strtolower(BackedEnum::class)])) {
$storage->declaring_property_ids['value'] = $storage->name;
$storage->appearing_property_ids['value'] = "{$storage->name}::\$value";
$storage->properties['value'] = new PropertyStorage();
$storage->properties['value']->type = new Union([new TInt(), new TString()]);
}
}

private function populateDataFromImplementedInterface(
Expand Down
33 changes: 30 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,6 +75,8 @@
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;
Expand Down Expand Up @@ -286,6 +290,7 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool
$this->codebase->classlikes->addFullyQualifiedTraitName($fq_classlike_name, $this->file_path);
} elseif ($node instanceof PhpParser\Node\Stmt\Enum_) {
$storage->is_enum = true;
$storage->final = true;

if ($node->scalarType) {
if ($node->scalarType->name === 'string' || $node->scalarType->name === 'int') {
Expand Down Expand Up @@ -1415,6 +1420,30 @@ 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) {
$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);
}
}
if ($valueTypes !== []) {
$storage->declaring_property_ids['value'] = $storage->name;
$storage->appearing_property_ids['value'] = "{$storage->name}::\$value";
$storage->properties['value'] = new PropertyStorage();
$storage->properties['value']->type = new Union($valueTypes);
}
}
}

/**
Expand Down Expand Up @@ -1580,9 +1609,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
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Psalm\Internal\Provider\ReturnTypeProvider;

use Psalm\CodeLocation;
Expand All @@ -18,8 +20,11 @@
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 strtolower;

Expand All @@ -28,14 +33,9 @@
*/
class GetObjectVarsReturnTypeProvider implements FunctionReturnTypeProviderInterface
{
/**
* @return array<lowercase-string>
*/
public static function getFunctionIds(): array
{
return [
'get_object_vars',
];
return ['get_object_vars'];
}

private static ?TArray $fallback = null;
Expand All @@ -55,6 +55,22 @@ 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)]);
}
return new TKeyedArray($properties);
}

if ($object_type instanceof TObjectWithProperties) {
if ([] === $object_type->properties) {
return self::$fallback;
Expand Down Expand Up @@ -126,7 +142,11 @@ 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
|| $codebase->interfaceExtends($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
132 changes: 132 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,135 @@ 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',
];
yield 'Interface extending UnitEnum' => [
'code' => <<<'PHP'
<?php
interface A extends UnitEnum {}
enum B implements A { case One; }
function getA(): A { return B::One; }
$b = get_object_vars(getA());
PHP,
'assertions' => [
'$b===' => 'array{name: non-empty-string}',
],
'ignored_issues' => [],
'php_version' => '8.1',
];
yield 'Interface extending BackedEnum' => [
'code' => <<<'PHP'
<?php
interface A extends BackedEnum {}
enum B: int implements A { case One = 1; }
function getA(): A { return B::One; }
$b = get_object_vars(getA());
PHP,
'assertions' => [
'$b===' => 'array{name: non-empty-string, value: int|string}',
],
'ignored_issues' => [],
'php_version' => '8.1',
];
}
}

0 comments on commit baab078

Please sign in to comment.