From 1e9ce3b414cc1737c6cde452857931f23c7b9d58 Mon Sep 17 00:00:00 2001 From: Valentin Udaltsov Date: Mon, 12 Aug 2024 06:14:06 +0300 Subject: [PATCH] Add ClassReflection::typeArgumentsOf($class) and resolvedTypeArgumentsOf($class) --- src/Reflection/CHANGELOG.md | 2 + src/Reflection/ClassReflection.php | 47 +++++++++++++++++++ .../class/resolved_type_arguments_of.php | 32 +++++++++++++ .../class/type_arguments_of.php | 42 +++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 tests/Reflection/functional_tests/class/resolved_type_arguments_of.php create mode 100644 tests/Reflection/functional_tests/class/type_arguments_of.php diff --git a/src/Reflection/CHANGELOG.md b/src/Reflection/CHANGELOG.md index d5c74f81..588642fc 100644 --- a/src/Reflection/CHANGELOG.md +++ b/src/Reflection/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `ConstantReflection` and `TyphoonReflector::reflectConstant()`. +- Add `ClassReflection::typeArgumentsOf($class)`. +- Add `ClassReflection::resolvedTypeArgumentsOf($class)`. ## [0.4.2] 2024-08-05 diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index d4655c3c..391a7b68 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -13,6 +13,7 @@ use Typhoon\Reflection\Internal\Misc\NonSerializable; use Typhoon\Reflection\Internal\NativeAdapter\ClassAdapter; use Typhoon\Type\Type; +use Typhoon\Type\types; use Typhoon\Type\Visitor\TemplateTypeResolver; use Typhoon\TypedMap\TypedMap; @@ -199,6 +200,52 @@ public function isInstanceOf(string|NamedClassId|AnonymousClassId $class): bool || \array_key_exists($class->name, $this->data[Data::Interfaces]); } + /** + * @param non-empty-string|NamedClassId|AnonymousClassId $class + * @return list + */ + public function typeArgumentsOf(string|NamedClassId|AnonymousClassId $class): array + { + if (\is_string($class)) { + $class = Id::class($class); + } + + if ($this->id->equals($class)) { + return $this + ->templates() + ->map(static fn(TemplateReflection $template): Type => types::template($template->id)) + ->toList(); + } + + if ($class instanceof AnonymousClassId) { + return []; + } + + /** @psalm-suppress PossiblyInvalidArrayOffset */ + return $this->data[Data::Parents][$class->name] ?? $this->data[Data::Interfaces][$class->name] ?? []; + } + + /** + * @param non-empty-string|NamedClassId|AnonymousClassId $class + * @param list $typeArguments + * @return list + */ + public function resolvedTypeArgumentsOf(string|NamedClassId|AnonymousClassId $class, array $typeArguments = []): array + { + $classTypeArguments = $this->typeArgumentsOf($class); + + if ($classTypeArguments === []) { + return []; + } + + $templateResolver = $this->createTemplateResolver($typeArguments); + + return array_map( + static fn(Type $type): Type => $type->accept($templateResolver), + $classTypeArguments, + ); + } + public function isClass(): bool { return $this->data[Data::ClassKind] === ClassKind::Class_; diff --git a/tests/Reflection/functional_tests/class/resolved_type_arguments_of.php b/tests/Reflection/functional_tests/class/resolved_type_arguments_of.php new file mode 100644 index 00000000..322d5c1b --- /dev/null +++ b/tests/Reflection/functional_tests/class/resolved_type_arguments_of.php @@ -0,0 +1,32 @@ +reflectClass(\Iterator::class); + assertEquals([types::mixed, types::mixed], $iterator->resolvedTypeArgumentsOf(\Iterator::class)); + assertEquals([types::int, types::string], $iterator->resolvedTypeArgumentsOf(\Iterator::class, [types::int, types::string])); + assertEquals([types::mixed, types::mixed], $iterator->resolvedTypeArgumentsOf(\Traversable::class)); + assertEquals([types::int, types::string], $iterator->resolvedTypeArgumentsOf(\Traversable::class, [types::int, types::string])); + + $a = $reflector + ->withResource(Resource::fromCode( + <<<'PHP' + + */ + abstract class A implements Iterator {} + PHP, + )) + ->reflectClass('A'); + assertEquals([types::mixed, types::string], $a->resolvedTypeArgumentsOf(\Iterator::class)); + assertEquals([types::mixed, types::string], $a->resolvedTypeArgumentsOf(\Traversable::class)); +}; diff --git a/tests/Reflection/functional_tests/class/type_arguments_of.php b/tests/Reflection/functional_tests/class/type_arguments_of.php new file mode 100644 index 00000000..44b1d69c --- /dev/null +++ b/tests/Reflection/functional_tests/class/type_arguments_of.php @@ -0,0 +1,42 @@ +reflectClass(\Iterator::class); + assertEquals( + [types::classTemplate(\Iterator::class, 'TKey'), types::classTemplate(\Iterator::class, 'TValue')], + $iterator->typeArgumentsOf(\Iterator::class), + ); + assertEquals( + [types::classTemplate(\Iterator::class, 'TKey'), types::classTemplate(\Iterator::class, 'TValue')], + $iterator->typeArgumentsOf(\Traversable::class), + ); + + $a = $reflector + ->withResource(Resource::fromCode( + <<<'PHP' + + */ + abstract class A implements Iterator {} + PHP, + )) + ->reflectClass('A'); + assertEquals( + [types::mixed, types::string], + $a->typeArgumentsOf(\Iterator::class), + ); + assertEquals( + [types::mixed, types::string], + $a->typeArgumentsOf(\Traversable::class), + ); +};