diff --git a/src/PseudoTypes/Generic.php b/src/PseudoTypes/Generic.php new file mode 100644 index 00000000..21e27ac3 --- /dev/null +++ b/src/PseudoTypes/Generic.php @@ -0,0 +1,56 @@ +types = $types; + } + + /** + * @return Type[] + */ + public function getTypes(): array + { + return $this->types; + } + + public function __toString(): string + { + $objectType = (string) ($this->fqsen ?? 'object'); + + return $objectType . '<' . implode(', ', $this->types) . '>'; + } +} diff --git a/src/PseudoTypes/GenericTemplate.php b/src/PseudoTypes/GenericTemplate.php new file mode 100644 index 00000000..52ca6086 --- /dev/null +++ b/src/PseudoTypes/GenericTemplate.php @@ -0,0 +1,43 @@ +resolvedType = $resolvedType; + } + + public function getResolvedType(): Object_ + { + return $this->resolvedType; + } + + public function __toString(): string + { + return (string) $this->resolvedType; + } +} diff --git a/src/TypeResolver.php b/src/TypeResolver.php index de20a1dd..e2b8089b 100644 --- a/src/TypeResolver.php +++ b/src/TypeResolver.php @@ -23,6 +23,8 @@ use phpDocumentor\Reflection\PseudoTypes\ConstExpression; use phpDocumentor\Reflection\PseudoTypes\False_; use phpDocumentor\Reflection\PseudoTypes\FloatValue; +use phpDocumentor\Reflection\PseudoTypes\Generic; +use phpDocumentor\Reflection\PseudoTypes\GenericTemplate; use phpDocumentor\Reflection\PseudoTypes\HtmlEscapedString; use phpDocumentor\Reflection\PseudoTypes\IntegerRange; use phpDocumentor\Reflection\PseudoTypes\IntegerValue; @@ -56,7 +58,6 @@ use phpDocumentor\Reflection\Types\Callable_; use phpDocumentor\Reflection\Types\CallableParameter; use phpDocumentor\Reflection\Types\ClassString; -use phpDocumentor\Reflection\Types\Collection; use phpDocumentor\Reflection\Types\Compound; use phpDocumentor\Reflection\Types\Context; use phpDocumentor\Reflection\Types\Expression; @@ -460,15 +461,24 @@ private function createFromGeneric(GenericTypeNode $type, Context $context): Typ return new Self_(...$this->createTypesByTypeNodes($type->genericTypes, $context)); default: - $collectionType = $this->createType($type->type, $context); - if ($collectionType instanceof Object_ === false) { - throw new RuntimeException(sprintf('%s is not a collection', (string) $collectionType)); + $mainType = $this->createType($type->type, $context); + if ($mainType instanceof Object_ === false) { + throw new RuntimeException(sprintf('%s is an unsupported generic', (string) $mainType)); } - return new Collection( - $collectionType->getFqsen(), - ...array_reverse($this->createTypesByTypeNodes($type->genericTypes, $context)) + $types = array_map( + function (TypeNode $node) use ($context): Type { + $innerType = $this->createType($node, $context); + if ($innerType instanceof Object_ && $innerType instanceof Generic === false) { + return new GenericTemplate($innerType); + } + + return $innerType; + }, + $type->genericTypes ); + + return new Generic($mainType->getFqsen(), $types); } } diff --git a/src/Types/AbstractList.php b/src/Types/AbstractList.php index 68c1f88c..32eb0f23 100644 --- a/src/Types/AbstractList.php +++ b/src/Types/AbstractList.php @@ -16,7 +16,7 @@ use phpDocumentor\Reflection\Type; /** - * Represents a list of values. This is an abstract class for Array_ and Collection. + * Represents a list of values. This is an abstract class for Array_ and List_. * * @psalm-immutable */ diff --git a/src/Types/Collection.php b/src/Types/Collection.php deleted file mode 100644 index 0963787a..00000000 --- a/src/Types/Collection.php +++ /dev/null @@ -1,69 +0,0 @@ -` - * 2. `ACollectionObject` - * - * - ACollectionObject can be 'array' or an object that can act as an array - * - aValueType and aKeyType can be any type expression - * - * @psalm-immutable - */ -final class Collection extends AbstractList -{ - /** @var Fqsen|null */ - private $fqsen; - - /** - * Initializes this representation of an array with the given Type or Fqsen. - */ - public function __construct(?Fqsen $fqsen, Type $valueType, ?Type $keyType = null) - { - parent::__construct($valueType, $keyType); - - $this->fqsen = $fqsen; - } - - /** - * Returns the FQSEN associated with this object. - */ - public function getFqsen(): ?Fqsen - { - return $this->fqsen; - } - - /** - * Returns a rendered output of the Type as it would be used in a DocBlock. - */ - public function __toString(): string - { - $objectType = (string) ($this->fqsen ?? 'object'); - $valueType = $this->getValueType(); - - if ($this->keyType === null) { - return $objectType . '<' . $valueType . '>'; - } - - return $objectType . '<' . $this->keyType . ',' . $valueType . '>'; - } -} diff --git a/src/Types/Object_.php b/src/Types/Object_.php index 90dee57a..649f945e 100644 --- a/src/Types/Object_.php +++ b/src/Types/Object_.php @@ -28,10 +28,10 @@ * * @psalm-immutable */ -final class Object_ implements Type +class Object_ implements Type { /** @var Fqsen|null */ - private $fqsen; + protected $fqsen; /** * Initializes this object with an optional FQSEN, if not provided this object is considered 'untyped'. diff --git a/tests/unit/CollectionResolverTest.php b/tests/unit/CollectionResolverTest.php index 967c1156..7f7db8ab 100644 --- a/tests/unit/CollectionResolverTest.php +++ b/tests/unit/CollectionResolverTest.php @@ -13,10 +13,11 @@ namespace phpDocumentor\Reflection; +use phpDocumentor\Reflection\PseudoTypes\Generic; +use phpDocumentor\Reflection\PseudoTypes\GenericTemplate; use phpDocumentor\Reflection\PseudoTypes\List_; use phpDocumentor\Reflection\PseudoTypes\NonEmptyList; use phpDocumentor\Reflection\Types\Array_; -use phpDocumentor\Reflection\Types\Collection; use phpDocumentor\Reflection\Types\Compound; use phpDocumentor\Reflection\Types\Context; use phpDocumentor\Reflection\Types\Float_; @@ -36,7 +37,7 @@ class CollectionResolverTest extends TestCase /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection + * @uses \phpDocumentor\Reflection\Types\Generic * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::resolve @@ -49,23 +50,16 @@ public function testResolvingCollection(): void $resolvedType = $fixture->resolve('ArrayObject', new Context('')); - $this->assertInstanceOf(Collection::class, $resolvedType); + $this->assertInstanceOf(Generic::class, $resolvedType); $this->assertSame('\\ArrayObject', (string) $resolvedType); - - $this->assertEquals('\\ArrayObject', (string) $resolvedType->getFqsen()); - - $valueType = $resolvedType->getValueType(); - - $keyType = $resolvedType->getKeyType(); - - $this->assertInstanceOf(String_::class, $valueType); - $this->assertInstanceOf(Compound::class, $keyType); + $this->assertSame('\\ArrayObject', (string) $resolvedType->getFqsen()); + $this->assertEquals([new String_()], $resolvedType->getTypes()); } /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection + * @uses \phpDocumentor\Reflection\Types\Generic * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::__construct @@ -78,25 +72,21 @@ public function testResolvingCollectionWithKeyType(): void $resolvedType = $fixture->resolve('ArrayObject', new Context('')); - $this->assertInstanceOf(Collection::class, $resolvedType); - $this->assertSame('\\ArrayObject', (string) $resolvedType); + $this->assertInstanceOf(Generic::class, $resolvedType); + $this->assertSame('\\ArrayObject', (string) $resolvedType); + $this->assertSame('\\ArrayObject', (string) $resolvedType->getFqsen()); - $this->assertEquals('\\ArrayObject', (string) $resolvedType->getFqsen()); + $types = $resolvedType->getTypes(); - $valueType = $resolvedType->getValueType(); - - $keyType = $resolvedType->getKeyType(); - - $this->assertInstanceOf(Object_::class, $valueType); - $this->assertEquals('\\Iterator', (string) $valueType->getFqsen()); - $this->assertInstanceOf(Array_::class, $keyType); - $this->assertInstanceOf(String_::class, $keyType->getValueType()); + $this->assertArrayHasKey(0, $types); + $this->assertEquals(new Array_(new String_()), $types[0]); + $this->assertArrayHasKey(1, $types); + $this->assertEquals(new GenericTemplate(new Object_(new Fqsen('\\Iterator'))), $types[1]); } /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::__construct @@ -123,7 +113,6 @@ public function testResolvingArrayCollection(): void /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::__construct @@ -150,7 +139,6 @@ public function testResolvingArrayCollectionWithKey(): void /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection * @uses \phpDocumentor\Reflection\Types\String_ * @covers ::__construct @@ -177,7 +165,7 @@ public function testResolvingArrayCollectionWithKeyAndWhitespace(): void /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection + * @uses \phpDocumentor\Reflection\Types\Generic * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::__construct @@ -190,25 +178,22 @@ public function testResolvingCollectionOfCollection(): void $resolvedType = $fixture->resolve('ArrayObject>', new Context('')); - $this->assertInstanceOf(Collection::class, $resolvedType); - $this->assertSame('\\ArrayObject>', (string) $resolvedType); + $this->assertInstanceOf(Generic::class, $resolvedType); + $this->assertSame('\\ArrayObject>', (string) $resolvedType); + $this->assertSame('\\ArrayObject', (string) $resolvedType->getFqsen()); - $this->assertEquals('\\ArrayObject', (string) $resolvedType->getFqsen()); + $types = $resolvedType->getTypes(); - $valueType = $resolvedType->getValueType(); - $this->assertInstanceOf(Collection::class, $valueType); - $collectionValueType = $valueType->getValueType(); + $this->assertArrayHasKey(0, $types); + $this->assertEquals(new Compound([new String_(), new Integer(), new Float_()]), $types[0]); - $this->assertInstanceOf(Object_::class, $valueType->getValueType()); - $this->assertEquals('\\ArrayObject', (string) $valueType->getFqsen()); - $this->assertInstanceOf(Object_::class, $collectionValueType); - $this->assertEquals('\\DateTime', (string) $collectionValueType->getFqsen()); + $this->assertArrayHasKey(0, $types); + $this->assertInstanceOf(Generic::class, $types[1]); + $this->assertSame('\\ArrayObject', (string) $types[1]->getFqsen()); - $keyType = $resolvedType->getKeyType(); - $this->assertInstanceOf(Compound::class, $keyType); - $this->assertInstanceOf(String_::class, $keyType->get(0)); - $this->assertInstanceOf(Integer::class, $keyType->get(1)); - $this->assertInstanceOf(Float_::class, $keyType->get(2)); + $nestedGenericTypes = $types[1]->getTypes(); + $this->assertArrayHasKey(0, $nestedGenericTypes); + $this->assertEquals(new GenericTemplate(new Object_(new Fqsen('\\DateTime'))), $nestedGenericTypes[0]); } /** @@ -278,7 +263,7 @@ public function testMissingEndCollection(): void public function testBadCollectionClass(): void { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('string is not a collection'); + $this->expectExceptionMessage('string is an unsupported generic'); $fixture = new TypeResolver(); $fixture->resolve('string', new Context('')); } @@ -286,7 +271,6 @@ public function testBadCollectionClass(): void /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::__construct diff --git a/tests/unit/IntegerRangeResolverTest.php b/tests/unit/IntegerRangeResolverTest.php index cfe35f6a..d53c7ec3 100644 --- a/tests/unit/IntegerRangeResolverTest.php +++ b/tests/unit/IntegerRangeResolverTest.php @@ -27,7 +27,6 @@ class IntegerRangeResolverTest extends TestCase /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::__construct @@ -53,7 +52,6 @@ public function testResolvingIntRange(): void /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::__construct @@ -79,7 +77,6 @@ public function testResolvingIntRangeWithKeywords(): void /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::__construct @@ -98,7 +95,6 @@ public function testResolvingIntRangeErrorMissingMaxValue(): void /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::__construct @@ -117,7 +113,6 @@ public function testResolvingIntRangeErrorMisingMinValue(): void /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::__construct @@ -136,7 +131,6 @@ public function testResolvingIntRangeErrorMisingComma(): void /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::__construct diff --git a/tests/unit/NumericResolverTest.php b/tests/unit/NumericResolverTest.php index 20284b66..6a8c935e 100644 --- a/tests/unit/NumericResolverTest.php +++ b/tests/unit/NumericResolverTest.php @@ -28,7 +28,6 @@ class NumericResolverTest extends TestCase /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\Collection * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::__construct diff --git a/tests/unit/PseudoTypes/GenericTemplateTest.php b/tests/unit/PseudoTypes/GenericTemplateTest.php new file mode 100644 index 00000000..6cfb02fb --- /dev/null +++ b/tests/unit/PseudoTypes/GenericTemplateTest.php @@ -0,0 +1,35 @@ +assertSame($resolvedType, $type->getResolvedType()); + } + + /** + * @covers ::__toString + */ + public function testToString(): void + { + $type = new GenericTemplate(new Object_(new Fqsen('\\Foo\\SomeClass'))); + $this->assertSame('\\Foo\\SomeClass', (string) $type); + } +} diff --git a/tests/unit/PseudoTypes/GenericTest.php b/tests/unit/PseudoTypes/GenericTest.php new file mode 100644 index 00000000..2da410cf --- /dev/null +++ b/tests/unit/PseudoTypes/GenericTest.php @@ -0,0 +1,81 @@ +assertSame($fqsen, $type->getFqsen()); + $this->assertSame($types, $type->getTypes()); + } + + /** + * @dataProvider provideToStringData + * @covers ::__toString + */ + public function testToString(string $expectedResult, Generic $type): void + { + $this->assertSame($expectedResult, (string) $type); + } + + /** + * @return array + */ + public static function provideToStringData(): array + { + return [ + 'without fqsen' => [ + 'object', + new Generic( + null, + [ + new String_(), + ] + ), + ], + 'collection without key' => [ + '\\ArrayObject', + new Generic(new Fqsen('\\ArrayObject'), [new String_()]), + ], + 'collection with key' => [ + '\\ArrayObject', + new Generic( + new Fqsen('\\ArrayObject'), + [new Array_(new String_()), new Object_(new Fqsen('\\Iterator'))] + ), + ], + 'more than two generics' => [ + '\\MyClass<\\T, \\SomeClassSecond, \\SomeClassThird>', + new Generic( + new Fqsen('\\MyClass'), + [ + new GenericTemplate(new Object_(new Fqsen('\\T'))), + new Object_(new Fqsen('\\SomeClassSecond')), + new Object_(new Fqsen('\\SomeClassThird')), + ] + ), + ], + ]; + } +} diff --git a/tests/unit/TypeResolverTest.php b/tests/unit/TypeResolverTest.php index 56cc23c0..4cff6369 100644 --- a/tests/unit/TypeResolverTest.php +++ b/tests/unit/TypeResolverTest.php @@ -23,6 +23,7 @@ use phpDocumentor\Reflection\PseudoTypes\ConstExpression; use phpDocumentor\Reflection\PseudoTypes\False_; use phpDocumentor\Reflection\PseudoTypes\FloatValue; +use phpDocumentor\Reflection\PseudoTypes\Generic; use phpDocumentor\Reflection\PseudoTypes\HtmlEscapedString; use phpDocumentor\Reflection\PseudoTypes\IntegerRange; use phpDocumentor\Reflection\PseudoTypes\IntegerValue; @@ -55,7 +56,6 @@ use phpDocumentor\Reflection\Types\Callable_; use phpDocumentor\Reflection\Types\CallableParameter; use phpDocumentor\Reflection\Types\ClassString; -use phpDocumentor\Reflection\Types\Collection; use phpDocumentor\Reflection\Types\Compound; use phpDocumentor\Reflection\Types\Context; use phpDocumentor\Reflection\Types\Expression; @@ -1117,10 +1117,12 @@ public function genericsProvider(): array [ 'Collection[]', new Array_( - new Collection( + new Generic( new Fqsen('\\phpDocumentor\\Collection'), - new Integer(), - new ArrayKey() + [ + new ArrayKey(), + new Integer(), + ] ) ), ], diff --git a/tests/unit/Types/CollectionTest.php b/tests/unit/Types/CollectionTest.php deleted file mode 100644 index c48f3f2c..00000000 --- a/tests/unit/Types/CollectionTest.php +++ /dev/null @@ -1,57 +0,0 @@ -assertSame($expectedString, (string) $collection); - } - - /** - * @return mixed[] - */ - public function provideCollections(): array - { - return [ - 'simple collection' => [ - new Collection(null, new Integer()), - 'object', - ], - 'simple collection with key type' => [ - new Collection(null, new Integer(), new String_()), - 'object', - ], - 'collection of single type using specific class' => [ - new Collection(new Fqsen('\Foo\Bar'), new Integer()), - '\Foo\Bar', - ], - 'collection of single type with key type and using specific class' => [ - new Collection(new Fqsen('\Foo\Bar'), new String_(), new Integer()), - '\Foo\Bar', - ], - ]; - } -}