From 3d6ce9f80d9e9f07c74c7e31cac3044313670c05 Mon Sep 17 00:00:00 2001 From: lmoelleken Date: Sat, 5 Feb 2022 01:52:47 +0100 Subject: [PATCH] fix for multiple class-string classes e.g.: `class-string<\Foo\Bar|\Foo\Lall>` --- src/TypeResolver.php | 54 ++++++++++++++--- src/Types/AbstractList.php | 27 +++++++++ src/Types/AggregatedType.php | 1 - tests/unit/ClassStringResolverTest.php | 83 ++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 tests/unit/ClassStringResolverTest.php diff --git a/src/TypeResolver.php b/src/TypeResolver.php index 0c9a73c..dc8dd81 100644 --- a/src/TypeResolver.php +++ b/src/TypeResolver.php @@ -17,6 +17,7 @@ use InvalidArgumentException; use phpDocumentor\Reflection\PseudoTypes\IntegerRange; use phpDocumentor\Reflection\PseudoTypes\List_; +use phpDocumentor\Reflection\Types\AggregatedType; use phpDocumentor\Reflection\Types\Array_; use phpDocumentor\Reflection\Types\ArrayKey; use phpDocumentor\Reflection\Types\ClassString; @@ -36,6 +37,7 @@ use function array_key_exists; use function array_pop; use function array_values; +use function assert; use function class_exists; use function class_implements; use function count; @@ -261,7 +263,9 @@ private function parseTypes(ArrayIterator $tokens, Context $context, int $parser $classType = array_pop($types); if ($classType !== null) { if ((string) $classType === 'class-string') { - $types[] = $this->resolveClassString($tokens, $context); + foreach ($this->resolveClassString($tokens, $context) as $classStringType) { + $types[] = $classStringType; + } } elseif ((string) $classType === 'int') { $types[] = $this->resolveIntRange($tokens); } elseif ((string) $classType === 'interface-string') { @@ -461,17 +465,41 @@ private function resolveTypedObject(string $type, ?Context $context = null): Obj * Resolves class string * * @param ArrayIterator $tokens + * + * @return array */ - private function resolveClassString(ArrayIterator $tokens, Context $context): Type + private function resolveClassString(ArrayIterator $tokens, Context $context): array { $tokens->next(); $classType = $this->parseTypes($tokens, $context, self::PARSER_IN_COLLECTION_EXPRESSION); - if (!$classType instanceof Object_ || $classType->getFqsen() === null) { - throw new RuntimeException( - $classType . ' is not a class string' - ); + $aggreatedObjects = false; + if ($classType instanceof AggregatedType) { + foreach ($classType->getIterator() as $typeInner) { + if (!$typeInner instanceof Object_) { + break; + } + } + + $aggreatedObjects = true; + } + + if ($aggreatedObjects) { + assert($classType instanceof AggregatedType); + foreach ($classType->getIterator() as $typeInner) { + if (!$typeInner instanceof Object_ || $typeInner->getFqsen() === null) { + throw new RuntimeException( + $typeInner . ' is not a class string' + ); + } + } + } else { + if (!$classType instanceof Object_ || $classType->getFqsen() === null) { + throw new RuntimeException( + $classType . ' is not a class string' + ); + } } $token = $tokens->current(); @@ -487,7 +515,19 @@ private function resolveClassString(ArrayIterator $tokens, Context $context): Ty ); } - return new ClassString($classType->getFqsen()); + $return = []; + if ($aggreatedObjects) { + assert($classType instanceof AggregatedType); + foreach ($classType->getIterator() as $typeInner) { + assert($typeInner instanceof Object_); + $return[] = new ClassString($typeInner->getFqsen()); + } + } else { + assert($classType instanceof Object_); + $return[] = new ClassString($classType->getFqsen()); + } + + return $return; } /** diff --git a/src/Types/AbstractList.php b/src/Types/AbstractList.php index b674862..1b8ca31 100644 --- a/src/Types/AbstractList.php +++ b/src/Types/AbstractList.php @@ -15,6 +15,9 @@ use phpDocumentor\Reflection\Type; +use function count; +use function implode; + /** * Represents a list of values. This is an abstract class for Array_ and Collection. * @@ -67,6 +70,30 @@ public function getValueType(): Type public function __toString(): string { if ($this->keyType) { + $classStringValues = []; + if ($this->valueType instanceof AggregatedType) { + /** + * @psalm-suppress ImpureMethodCall + */ + foreach ($this->valueType->getIterator() as $typeInner) { + if (!($typeInner instanceof ClassString)) { + $classStringValues = []; + break; + } + + $fqsenTmp = $typeInner->getFqsen(); + if (!$fqsenTmp) { + continue; + } + + $classStringValues[] = $fqsenTmp->__toString(); + } + } + + if (count($classStringValues) > 0) { + return 'array<' . $this->keyType . ',class-string<' . implode('|', $classStringValues) . '>>'; + } + return 'array<' . $this->keyType . ',' . $this->valueType . '>'; } diff --git a/src/Types/AggregatedType.php b/src/Types/AggregatedType.php index 472a1cd..51d1a97 100644 --- a/src/Types/AggregatedType.php +++ b/src/Types/AggregatedType.php @@ -77,7 +77,6 @@ public function has(int $index): bool public function contains(Type $type): bool { foreach ($this->types as $typePart) { - // if the type is duplicate; do not add it if ((string) $typePart === (string) $type) { return true; } diff --git a/tests/unit/ClassStringResolverTest.php b/tests/unit/ClassStringResolverTest.php new file mode 100644 index 0000000..8e748a7 --- /dev/null +++ b/tests/unit/ClassStringResolverTest.php @@ -0,0 +1,83 @@ + + * @coversDefaultClass \phpDocumentor\Reflection\TypeResolver + */ +class ClassStringResolverTest extends TestCase +{ + /** + * @uses \phpDocumentor\Reflection\Types\Array_ + * @uses \phpDocumentor\Reflection\Types\Integer + * @uses \phpDocumentor\Reflection\Types\ClassString + * + * @covers ::__construct + * @covers ::resolve + */ + public function testResolvingClassString(): void + { + $fixture = new TypeResolver(); + + $resolvedType = $fixture->resolve('array>', new Context('')); + + $this->assertInstanceOf(Array_::class, $resolvedType); + $this->assertSame('array>', (string) $resolvedType); + + $keyType = $resolvedType->getKeyType(); + $valueTpye = $resolvedType->getValueType(); + + $this->assertInstanceOf(Integer::class, $keyType); + + $this->assertInstanceOf(ClassString::class, $valueTpye); + $this->assertSame('Bar', $valueTpye->getFqsen()->getName()); + $this->assertSame('\Foo\Bar', $valueTpye->getFqsen()->__toString()); + } + + /** + * @uses \phpDocumentor\Reflection\Types\Array_ + * @uses \phpDocumentor\Reflection\Types\Integer + * @uses \phpDocumentor\Reflection\Types\ClassString + * + * @covers ::__construct + * @covers ::resolve + */ + public function testResolvingClassStrings(): void + { + $fixture = new TypeResolver(); + + $resolvedType = $fixture->resolve('array>', new Context('')); + + $this->assertInstanceOf(Array_::class, $resolvedType); + $this->assertSame('array>', (string) $resolvedType); + + $keyType = $resolvedType->getKeyType(); + $valueTpye = $resolvedType->getValueType(); + + $this->assertInstanceOf(Integer::class, $keyType); + + $this->assertInstanceOf(Compound::class, $valueTpye); + foreach ($valueTpye->getIterator() as $type) { + $this->assertInstanceOf(ClassString::class, $type); + } + } +}