diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 2b891ea1731..fac813b527e 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,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; @@ -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); + } } /** @@ -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; } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php index 8aae384cc07..27c5675babb 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php @@ -2,6 +2,8 @@ namespace Psalm\Internal\Provider\ReturnTypeProvider; +use BackedEnum; +use LogicException; use Psalm\CodeLocation; use Psalm\Context; use Psalm\Internal\Analyzer\ClassAnalyzer; @@ -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; @@ -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; /** @@ -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; @@ -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()], ); } } 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', + ]; } }