diff --git a/src/Reflection/Domain/Factories/TypeReflectionFactory.php b/src/Reflection/Domain/Factories/TypeReflectionFactory.php index b3ed010..fb7db9c 100644 --- a/src/Reflection/Domain/Factories/TypeReflectionFactory.php +++ b/src/Reflection/Domain/Factories/TypeReflectionFactory.php @@ -13,7 +13,24 @@ use phpDocumentor\Reflection\DocBlock\Tags\Return_; use phpDocumentor\Reflection\DocBlock\Tags\Var_; use phpDocumentor\Reflection\DocBlockFactory; +use phpDocumentor\Reflection\Fqsen; +use phpDocumentor\Reflection\PseudoType; +use phpDocumentor\Reflection\PseudoTypes\False_; +use phpDocumentor\Reflection\PseudoTypes\True_; use phpDocumentor\Reflection\Type as PhpDocumentorReflectionType; +use phpDocumentor\Reflection\Types\AbstractList; +use phpDocumentor\Reflection\Types\Array_; +use phpDocumentor\Reflection\Types\Collection; +use phpDocumentor\Reflection\Types\Compound; +use phpDocumentor\Reflection\Types\ContextFactory; +use phpDocumentor\Reflection\Types\Intersection; +use phpDocumentor\Reflection\Types\Iterable_; +use phpDocumentor\Reflection\Types\Nullable; +use phpDocumentor\Reflection\Types\Object_; +use phpDocumentor\Reflection\Types\Parent_; +use phpDocumentor\Reflection\Types\Self_; +use phpDocumentor\Reflection\Types\Static_; +use phpDocumentor\Reflection\Types\This; class TypeReflectionFactory { @@ -53,93 +70,206 @@ public function create(?\ReflectionType $ref, ?PhpDocumentorReflectionType $docC private function createTypeReflectionBasedOnReflectionType(?\ReflectionType $ref): TypeReflection { - if ($ref instanceof \ReflectionNamedType) { - $name = $ref->getName(); - $flags = 0; - $isCollection = in_array($name, ['array', 'iterable']); + // if there is no reflection type + if (null === $ref) { + return NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN); + } + + // if the reflection type is a union + if ($ref instanceof \ReflectionUnionType) { + $types = array_map( + fn (\ReflectionType $type) => $this->createTypeReflectionBasedOnReflectionType($type), + $ref->getTypes() + ); + + return UnionTypeReflection::create($types); + } + + // if the reflection type is an intersection + if ($ref instanceof \ReflectionIntersectionType) { + $types = array_map( + fn (\ReflectionType $type) => $this->createTypeReflectionBasedOnReflectionType($type), + $ref->getTypes() + ); + + return IntersectionTypeReflection::create($types); + } - if ($ref->isBuiltin()) { - $flags |= NamedTypeReflection::IS_BUILT_IN; + // if the reflection is a collection or named + if (!$ref instanceof \ReflectionNamedType) { + throw new \LogicException('The package require an update. On this stage the ReflectionType has to be an ReflectionNamedType.'); + } + + /** @var \ReflectionNamedType $ref */ + $name = $ref->getName(); + $flags = 0; + $isCollection = in_array($name, ['array', 'iterable']); + + if ($ref->isBuiltin()) { + $flags |= NamedTypeReflection::IS_BUILT_IN; + } + + if (class_exists($name, false) && !enum_exists($name, false)) { + $flags |= NamedTypeReflection::IS_CLASS; + $class = (new \ReflectionClass($name)); + + if ($class->isAbstract()) { + $flags |= NamedTypeReflection::IS_ABSTRACT; } - if (class_exists($name, false) && !enum_exists($name, false)) { - $flags |= NamedTypeReflection::IS_CLASS; - $class = (new \ReflectionClass($name)); + if ($class->implementsInterface(\Traversable::class) || $class->implementsInterface(\ArrayAccess::class)) { + $isCollection = true; + } + } - if ($class->isAbstract()) { - $flags |= NamedTypeReflection::IS_ABSTRACT; - } + if (interface_exists($name, false)) { + $flags |= NamedTypeReflection::IS_INTERFACE; + $interface = (new \ReflectionClass($name)); - if ($class->implementsInterface(\Traversable::class) || $class->implementsInterface(\ArrayAccess::class)) { - $isCollection = true; - } + if ($interface->implementsInterface(\Traversable::class) || $interface->implementsInterface(\ArrayAccess::class)) { + $isCollection = true; } + } - if (interface_exists($name, false)) { - $flags |= NamedTypeReflection::IS_INTERFACE; - $interface = (new \ReflectionClass($name)); + if (enum_exists($name, false)) { + $flags |= NamedTypeReflection::IS_ENUM; + } - if ($interface->implementsInterface(\Traversable::class) || $interface->implementsInterface(\ArrayAccess::class)) { - $isCollection = true; - } + $output = $isCollection + ? CollectionTypeReflection::create( + NamedTypeReflection::create($name, $flags), + \SplObjectStorage::class === $name || (@$class && $class->isSubclassOf(\SplObjectStorage::class)) + ? NamedTypeReflection::create('object', NamedTypeReflection::IS_BUILT_IN) + : UnionTypeReflection::create([ + NamedTypeReflection::create('int', NamedTypeReflection::IS_BUILT_IN), + NamedTypeReflection::create('string', NamedTypeReflection::IS_BUILT_IN), + ]), + NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN), + ) + : NamedTypeReflection::create($name, $flags); + + // if the reflection type allows null + if ($ref->allowsNull()) { + if (!in_array($name, ['null', 'mixed'])) { + return UnionTypeReflection::create([ + NamedTypeReflection::create('null', NamedTypeReflection::IS_BUILT_IN), + $output, + ]); } - if (enum_exists($name, false)) { - $flags |= NamedTypeReflection::IS_ENUM; + if ('null' === $name) { + return NamedTypeReflection::create('null', NamedTypeReflection::IS_BUILT_IN); } - $output = $isCollection - ? CollectionTypeReflection::create( - NamedTypeReflection::create($name, $flags), - \SplObjectStorage::class === $name || (@$class && $class->isSubclassOf(\SplObjectStorage::class)) - ? NamedTypeReflection::create('object', NamedTypeReflection::IS_BUILT_IN) - : UnionTypeReflection::create([ - NamedTypeReflection::create('int', NamedTypeReflection::IS_BUILT_IN), - NamedTypeReflection::create('string', NamedTypeReflection::IS_BUILT_IN), - ]), - NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN), - ) - : NamedTypeReflection::create($name, $flags); - - if ($ref->allowsNull()) { - if (!in_array($name, ['null', 'mixed'])) { - return UnionTypeReflection::create([ - NamedTypeReflection::create('null', NamedTypeReflection::IS_BUILT_IN), - $output, - ]); - } - - if ('null' === $name) { - return NamedTypeReflection::create('null', NamedTypeReflection::IS_BUILT_IN); - } - - if ('mixed' === $name) { - return NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN); - } + if ('mixed' === $name) { + return NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN); } + } + + return $output; + } - return $output; + private function createTypeReflectionBasedOnPhpDocumentatorReflectionType(?PhpDocumentorReflectionType $ref, \ReflectionClass $declarationClass): TypeReflection + { + // if there is no reflection type + if (null === $ref) { + return NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN); } - if ($ref instanceof \ReflectionUnionType) { + // if the reflection type is a pseudo-type + if ($ref instanceof PseudoType && !$ref instanceof True_ && !$ref instanceof False_) { + $ref = $ref->underlyingType(); + } + + // if the reflection type is a union + if ($ref instanceof Compound) { $types = array_map( - fn (\ReflectionType $type) => $this->createTypeReflectionBasedOnReflectionType($type), - $ref->getTypes() + fn (PhpDocumentorReflectionType $type) => $this->createTypeReflectionBasedOnPhpDocumentatorReflectionType($type, $declarationClass), + $ref->getIterator()->getArrayCopy() ); return UnionTypeReflection::create($types); } - if ($ref instanceof \ReflectionIntersectionType) { + // if the reflection type is an intersection + if ($ref instanceof Intersection) { $types = array_map( - fn (\ReflectionType $type) => $this->createTypeReflectionBasedOnReflectionType($type), - $ref->getTypes() + fn (PhpDocumentorReflectionType $type) => $this->createTypeReflectionBasedOnPhpDocumentatorReflectionType($type, $declarationClass), + $ref->getIterator()->getArrayCopy() ); return IntersectionTypeReflection::create($types); } - return NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN); + // if the reflection is a collection + if ($ref instanceof AbstractList || in_array(get_class($ref), [Array_::class, Iterable_::class])) { + return CollectionTypeReflection::create( + match (get_class($ref)) { + Array_::class => NamedTypeReflection::create('array', NamedTypeReflection::IS_BUILT_IN), + Iterable_::class => NamedTypeReflection::create('iterable', NamedTypeReflection::IS_BUILT_IN), + Collection::class => NamedTypeReflection::create($ref->getFqsen()?->__toString() ?? 'object', NamedTypeReflection::IS_CLASS), + }, + $this->createTypeReflectionBasedOnPhpDocumentatorReflectionType($ref->getKeyType(), $declarationClass), + $this->createTypeReflectionBasedOnPhpDocumentatorReflectionType($ref->getValueType(), $declarationClass) + ); + } + + // it the reflection is object pseudo-type + $ref = match (get_class($ref)) { + Self_::class, Static_::class, This::class => new Object_(new Fqsen('\\'.ltrim($declarationClass->getName(), '\\'))), + Parent_::class => $declarationClass->getParentClass() + ? new Object_(new Fqsen('\\'.ltrim($declarationClass->getParentClass()->getName(), '\\'))) + : throw new \LogicException('The reflection is Parent but the class does not have a parent.'), + default => $ref, + }; + + $flags = 0; + // if the reflection is class, interface or enum + if ($ref instanceof Object_ && null !== $ref->getFqsen()) { + $class = (new \ReflectionClass($ref->getFqsen()->__toString())); + $isClass = true; + $isCollection = $class->implementsInterface(\Traversable::class) || $class->implementsInterface(\ArrayAccess::class); + if ($class->isAbstract()) { + $flags |= NamedTypeReflection::IS_ABSTRACT; + } + if ($class->isInterface()) { + $flags |= NamedTypeReflection::IS_INTERFACE; + $isClass = false; + } + if ($class->isEnum()) { + $flags |= NamedTypeReflection::IS_ENUM; + $isClass = false; + } + if ($isClass) { + $flags |= NamedTypeReflection::IS_CLASS; + } + + return $isCollection + ? CollectionTypeReflection::create( + NamedTypeReflection::create($class->getName(), $flags), + \SplObjectStorage::class === $class->getName() || $class->isSubclassOf(\SplObjectStorage::class) + ? NamedTypeReflection::create('object', NamedTypeReflection::IS_BUILT_IN) + : UnionTypeReflection::create([ + NamedTypeReflection::create('int', NamedTypeReflection::IS_BUILT_IN), + NamedTypeReflection::create('string', NamedTypeReflection::IS_BUILT_IN), + ]), + NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN), + ) + : NamedTypeReflection::create($class->getName(), $flags); + } + + // if the reflection allows null + if ($ref instanceof Nullable) { + return UnionTypeReflection::create([ + NamedTypeReflection::create('null', NamedTypeReflection::IS_BUILT_IN), + $this->createTypeReflectionBasedOnPhpDocumentatorReflectionType($ref->getActualType(), $declarationClass), + ]); + } + + // if the reflection is built-in + $flags |= NamedTypeReflection::IS_BUILT_IN; + + return NamedTypeReflection::create($ref->__toString(), $flags); } private function getDocBlockReflectionTypeFromVarTag(\ReflectionProperty $ref): ?PhpDocumentorReflectionType @@ -150,7 +280,8 @@ private function getDocBlockReflectionTypeFromVarTag(\ReflectionProperty $ref): } $factory = DocBlockFactory::createInstance(); - $docBlock = $factory->create($docBlock); + $context = (new ContextFactory())->createFromReflector($ref); + $docBlock = $factory->create($docBlock, $context); /** @var Var_[] $tags */ $tags = $docBlock->getTagsByName('var'); @@ -170,7 +301,8 @@ private function getDocBlockReflectionTypeFromReturnTag(\ReflectionMethod $ref): } $factory = DocBlockFactory::createInstance(); - $docBlock = $factory->create($docBlock); + $context = (new ContextFactory())->createFromReflector($ref); + $docBlock = $factory->create($docBlock, $context); /** @var Return_[] $tags */ $tags = $docBlock->getTagsByName('return'); @@ -190,7 +322,8 @@ private function getDocBlockReflectionTypeFromParamTag(\ReflectionParameter $ref } $factory = DocBlockFactory::createInstance(); - $docBlock = $factory->create($docBlock); + $context = (new ContextFactory())->createFromReflector($ref); + $docBlock = $factory->create($docBlock, $context); /** @var Param[] $tags */ $tags = $docBlock->getTagsByName('param'); diff --git a/tools/phpstan/fpm-baseline.neon b/tools/phpstan/fpm-baseline.neon index 1bbbf9c..dd00392 100644 --- a/tools/phpstan/fpm-baseline.neon +++ b/tools/phpstan/fpm-baseline.neon @@ -295,11 +295,36 @@ parameters: count: 1 path: ../../src/Reflection/Domain/Entities/Type/NamedTypeReflection.php + - + message: "#^Call to an undefined method phpDocumentor\\\\Reflection\\\\Type\\:\\:getKeyType\\(\\)\\.$#" + count: 1 + path: ../../src/Reflection/Domain/Factories/TypeReflectionFactory.php + + - + message: "#^Call to an undefined method phpDocumentor\\\\Reflection\\\\Type\\:\\:getValueType\\(\\)\\.$#" + count: 1 + path: ../../src/Reflection/Domain/Factories/TypeReflectionFactory.php + - message: "#^Left side of && is always true\\.$#" count: 1 path: ../../src/Reflection/Domain/Factories/TypeReflectionFactory.php + - + message: "#^Match expression does not handle remaining value\\: class\\-string\\$#" + count: 1 + path: ../../src/Reflection/Domain/Factories/TypeReflectionFactory.php + + - + message: "#^Method PBaszak\\\\UltraMapper\\\\Reflection\\\\Domain\\\\Factories\\\\TypeReflectionFactory\\:\\:createTypeReflectionBasedOnPhpDocumentatorReflectionType\\(\\) is unused\\.$#" + count: 1 + path: ../../src/Reflection/Domain/Factories/TypeReflectionFactory.php + + - + message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\\\|T of object, string given\\.$#" + count: 1 + path: ../../src/Reflection/Domain/Factories/TypeReflectionFactory.php + - message: "#^Variable \\$class might not be defined\\.$#" count: 1