1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Fix get_object_vars on enums

This commit is contained in:
Jack Worman 2022-12-21 09:06:11 -06:00
parent c75f06eeff
commit 8e5904d624
6 changed files with 219 additions and 12 deletions

View File

@ -109,8 +109,14 @@
"cs": "phpcs -ps",
"cs-fix": "phpcbf -ps",
"lint": "parallel-lint ./src ./tests",
"phpunit": "paratest --runner=WrapperRunner",
"phpunit-std": "phpunit",
"phpunit": [
"Composer\\Config::disableProcessTimeout",
"paratest --runner=WrapperRunner"
],
"phpunit-std": [
"Composer\\Config::disableProcessTimeout",
"phpunit"
],
"verify-callmap": "phpunit tests/Internal/Codebase/InternalCallMapHandlerTest.php",
"psalm": "@php ./psalm --find-dead-code",
"tests": [

View File

@ -2,6 +2,7 @@
namespace Psalm\Internal\Codebase;
use BackedEnum;
use InvalidArgumentException;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\MethodIdentifier;
@ -14,8 +15,13 @@ use Psalm\Progress\Progress;
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;
@ -688,6 +694,19 @@ class Populator
$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(

View File

@ -73,6 +73,8 @@ use function assert;
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;
@ -286,6 +288,7 @@ class ClassLikeNodeScanner
$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') {
@ -694,6 +697,33 @@ class ClassLikeNodeScanner
}
}
if ($storage->is_enum) {
$name_types = [];
$values_types = [];
foreach ($storage->enum_cases as $name => $enumCaseStorage) {
$name_types[] = new Type\Atomic\TLiteralString($name);
if ($storage->enum_type !== null) {
if (is_string($enumCaseStorage->value)) {
$values_types[] = new Type\Atomic\TLiteralString($enumCaseStorage->value);
} elseif (is_int($enumCaseStorage->value)) {
$values_types[] = new Type\Atomic\TLiteralInt($enumCaseStorage->value);
}
}
}
if ($name_types !== []) {
$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($name_types);
}
if ($values_types !== []) {
$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($values_types);
}
}
foreach ($node->stmts as $node_stmt) {
if ($node_stmt instanceof PhpParser\Node\Stmt\Property) {
$this->visitPropertyDeclaration($node_stmt, $this->config, $storage, $fq_classlike_name);
@ -1580,9 +1610,7 @@ class ClassLikeNodeScanner
}
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;
}
}

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Psalm\Internal\Provider\ReturnTypeProvider;
use Psalm\CodeLocation;
@ -18,8 +20,11 @@ use Psalm\Type\Atomic\TKeyedArray;
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;
@ -28,14 +33,9 @@ use function strtolower;
*/
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;
@ -55,6 +55,22 @@ class GetObjectVarsReturnTypeProvider implements FunctionReturnTypeProviderInter
$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;
@ -126,7 +142,11 @@ class GetObjectVarsReturnTypeProvider implements FunctionReturnTypeProviderInter
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()],
);
}
}

View File

@ -13,6 +13,8 @@ namespace {
interface BackedEnum extends UnitEnum
{
/** @var non-empty-string $name */
public readonly string $name;
public readonly int|string $value;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Psalm\Tests\ReturnTypeProvider;
use Psalm\Tests\TestCase;
@ -168,5 +170,135 @@ class GetObjectVarsTest extends TestCase
],
'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',
];
}
}