From 77418de99361c9814da69bdfff3ebdb6eb28f8ee Mon Sep 17 00:00:00 2001 From: Jack Worman Date: Wed, 21 Dec 2022 09:06:11 -0600 Subject: [PATCH] Fix get_object_vars on enums --- .../Reflector/ClassLikeNodeScanner.php | 32 +++++- .../GetObjectVarsReturnTypeProvider.php | 27 ++++- stubs/Php81.phpstub | 2 + .../ReturnTypeProvider/GetObjectVarsTest.php | 104 ++++++++++++++++++ 4 files changed, 161 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 2b891ea1731..d69f11f7f4b 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -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; @@ -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; @@ -1415,6 +1419,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); + } + } } /** @@ -1580,9 +1608,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; } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php index 8aae384cc07..010ee8e8d07 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Provider\ReturnTypeProvider; +use BackedEnum; use Psalm\CodeLocation; use Psalm\Context; use Psalm\Internal\Analyzer\ClassAnalyzer; @@ -18,8 +19,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; @@ -55,6 +59,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; @@ -126,7 +146,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()], ); } } diff --git a/stubs/Php81.phpstub b/stubs/Php81.phpstub index 06d6d0debc3..ad3f52d1b1c 100644 --- a/stubs/Php81.phpstub +++ b/stubs/Php81.phpstub @@ -13,6 +13,8 @@ namespace { interface BackedEnum extends UnitEnum { + /** @var non-empty-string $name */ + public readonly string $name; public readonly int|string $value; /** diff --git a/tests/ReturnTypeProvider/GetObjectVarsTest.php b/tests/ReturnTypeProvider/GetObjectVarsTest.php index ced035514bf..1d4da0bdee8 100644 --- a/tests/ReturnTypeProvider/GetObjectVarsTest.php +++ b/tests/ReturnTypeProvider/GetObjectVarsTest.php @@ -1,5 +1,7 @@ '8.2', ]; + + yield 'UnitEnum generic' => [ + 'code' => <<<'PHP' + [ + '$b===' => 'array{name: non-empty-string}', + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ]; + yield 'UnitEnum specific' => [ + 'code' => <<<'PHP' + [ + '$b===' => "array{name: 'One'|'Two'}", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ]; + yield 'UnitEnum literal' => [ + 'code' => <<<'PHP' + [ + '$b===' => "array{name: 'One'}", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ]; + yield 'BackedEnum generic' => [ + 'code' => <<<'PHP' + [ + '$b===' => 'array{name: non-empty-string, value: int|string}', + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ]; + yield 'Int BackedEnum specific' => [ + 'code' => <<<'PHP' + [ + '$b===' => "array{name: 'One'|'Two', value: 1|2}", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ]; + yield 'String BackedEnum specific' => [ + 'code' => <<<'PHP' + [ + '$b===' => "array{name: 'One'|'Two', value: 'one'|'two'}", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ]; + yield 'Int BackedEnum literal' => [ + 'code' => <<<'PHP' + [ + '$b===' => "array{name: 'One', value: 1}", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ]; + yield 'String BackedEnum literal' => [ + 'code' => <<<'PHP' + [ + '$b===' => "array{name: 'One', value: 'one'}", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ]; } }