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:
parent
c75f06eeff
commit
8e5904d624
@ -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": [
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ namespace {
|
||||
|
||||
interface BackedEnum extends UnitEnum
|
||||
{
|
||||
/** @var non-empty-string $name */
|
||||
public readonly string $name;
|
||||
public readonly int|string $value;
|
||||
|
||||
/**
|
||||
|
@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user