diff --git a/src/PseudoTypes/ArrayShapeItem.php b/src/PseudoTypes/ArrayShapeItem.php index a9756bb..81187d9 100644 --- a/src/PseudoTypes/ArrayShapeItem.php +++ b/src/PseudoTypes/ArrayShapeItem.php @@ -13,53 +13,6 @@ namespace phpDocumentor\Reflection\PseudoTypes; -use phpDocumentor\Reflection\Type; -use phpDocumentor\Reflection\Types\Mixed_; - -use function sprintf; - -final class ArrayShapeItem +class ArrayShapeItem extends ShapeItem { - /** @var string|null */ - private $key; - /** @var Type */ - private $value; - /** @var bool */ - private $optional; - - public function __construct(?string $key, ?Type $value, bool $optional) - { - $this->key = $key; - $this->value = $value ?? new Mixed_(); - $this->optional = $optional; - } - - public function getKey(): ?string - { - return $this->key; - } - - public function getValue(): Type - { - return $this->value; - } - - public function isOptional(): bool - { - return $this->optional; - } - - public function __toString(): string - { - if ($this->key !== null) { - return sprintf( - '%s%s: %s', - $this->key, - $this->optional ? '?' : '', - (string) $this->value - ); - } - - return (string) $this->value; - } } diff --git a/src/PseudoTypes/ListShape.php b/src/PseudoTypes/ListShape.php new file mode 100644 index 0000000..14f3e21 --- /dev/null +++ b/src/PseudoTypes/ListShape.php @@ -0,0 +1,15 @@ +getItems()) . '}'; + } +} diff --git a/src/PseudoTypes/ListShapeItem.php b/src/PseudoTypes/ListShapeItem.php new file mode 100644 index 0000000..d3eb7d7 --- /dev/null +++ b/src/PseudoTypes/ListShapeItem.php @@ -0,0 +1,9 @@ +items = $items; + } + + /** + * @return ObjectShapeItem[] + */ + public function getItems(): array + { + return $this->items; + } + + public function underlyingType(): Type + { + return new Object_(); + } + + public function __toString(): string + { + return 'object{' . implode(', ', $this->items) . '}'; + } +} diff --git a/src/PseudoTypes/ObjectShapeItem.php b/src/PseudoTypes/ObjectShapeItem.php new file mode 100644 index 0000000..3aaecfd --- /dev/null +++ b/src/PseudoTypes/ObjectShapeItem.php @@ -0,0 +1,9 @@ +key = $key; + $this->value = $value ?? new Mixed_(); + $this->optional = $optional; + } + + public function getKey(): ?string + { + return $this->key; + } + + public function getValue(): Type + { + return $this->value; + } + + public function isOptional(): bool + { + return $this->optional; + } + + public function __toString(): string + { + if ($this->key !== null) { + return sprintf( + '%s%s: %s', + $this->key, + $this->optional ? '?' : '', + (string) $this->value + ); + } + + return (string) $this->value; + } +} diff --git a/src/TypeResolver.php b/src/TypeResolver.php index 0c558c9..534d501 100644 --- a/src/TypeResolver.php +++ b/src/TypeResolver.php @@ -25,6 +25,8 @@ use phpDocumentor\Reflection\PseudoTypes\IntegerRange; use phpDocumentor\Reflection\PseudoTypes\IntegerValue; use phpDocumentor\Reflection\PseudoTypes\List_; +use phpDocumentor\Reflection\PseudoTypes\ListShape; +use phpDocumentor\Reflection\PseudoTypes\ListShapeItem; use phpDocumentor\Reflection\PseudoTypes\LiteralString; use phpDocumentor\Reflection\PseudoTypes\LowercaseString; use phpDocumentor\Reflection\PseudoTypes\NegativeInteger; @@ -33,6 +35,8 @@ use phpDocumentor\Reflection\PseudoTypes\NonEmptyString; use phpDocumentor\Reflection\PseudoTypes\Numeric_; use phpDocumentor\Reflection\PseudoTypes\NumericString; +use phpDocumentor\Reflection\PseudoTypes\ObjectShape; +use phpDocumentor\Reflection\PseudoTypes\ObjectShapeItem; use phpDocumentor\Reflection\PseudoTypes\PositiveInteger; use phpDocumentor\Reflection\PseudoTypes\StringValue; use phpDocumentor\Reflection\PseudoTypes\TraitString; @@ -82,6 +86,8 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; @@ -234,10 +240,43 @@ public function createType(?TypeNode $type, Context $context): Type ); case ArrayShapeNode::class: - return new ArrayShape( + switch ($type->kind) { + case ArrayShapeNode::KIND_ARRAY: + return new ArrayShape( + ...array_map( + function (ArrayShapeItemNode $item) use ($context): ArrayShapeItem { + return new ArrayShapeItem( + (string) $item->keyName, + $this->createType($item->valueType, $context), + $item->optional + ); + }, + $type->items + ) + ); + + case ArrayShapeNode::KIND_LIST: + return new ListShape( + ...array_map( + function (ArrayShapeItemNode $item) use ($context): ListShapeItem { + return new ListShapeItem( + null, + $this->createType($item->valueType, $context), + $item->optional + ); + }, + $type->items + ) + ); + + default: + throw new RuntimeException('Unsupported array shape kind'); + } + case ObjectShapeNode::class: + return new ObjectShape( ...array_map( - function (ArrayShapeItemNode $item) use ($context): ArrayShapeItem { - return new ArrayShapeItem( + function (ObjectShapeItemNode $item) use ($context): ObjectShapeItem { + return new ObjectShapeItem( (string) $item->keyName, $this->createType($item->valueType, $context), $item->optional diff --git a/tests/unit/CollectionResolverTest.php b/tests/unit/CollectionResolverTest.php index 7f480cd..967c115 100644 --- a/tests/unit/CollectionResolverTest.php +++ b/tests/unit/CollectionResolverTest.php @@ -40,6 +40,7 @@ class CollectionResolverTest extends TestCase * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::resolve + * @covers ::createType * @covers ::__construct */ public function testResolvingCollection(): void @@ -69,6 +70,7 @@ public function testResolvingCollection(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingCollectionWithKeyType(): void { @@ -99,6 +101,7 @@ public function testResolvingCollectionWithKeyType(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingArrayCollection(): void { @@ -125,6 +128,7 @@ public function testResolvingArrayCollection(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingArrayCollectionWithKey(): void { @@ -151,6 +155,7 @@ public function testResolvingArrayCollectionWithKey(): void * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingArrayCollectionWithKeyAndWhitespace(): void { @@ -177,6 +182,7 @@ public function testResolvingArrayCollectionWithKeyAndWhitespace(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingCollectionOfCollection(): void { @@ -208,6 +214,7 @@ public function testResolvingCollectionOfCollection(): void /** * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testBadArrayCollectionKey(): void { @@ -220,6 +227,7 @@ public function testBadArrayCollectionKey(): void /** * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testGoodArrayCollectionKey(): void { @@ -239,6 +247,7 @@ public function testGoodArrayCollectionKey(): void /** * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testMissingStartCollection(): void { @@ -251,6 +260,7 @@ public function testMissingStartCollection(): void /** * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testMissingEndCollection(): void { @@ -263,6 +273,7 @@ public function testMissingEndCollection(): void /** * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testBadCollectionClass(): void { @@ -280,6 +291,7 @@ public function testBadCollectionClass(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingCollectionAsArray(): void { @@ -304,6 +316,7 @@ public function testResolvingCollectionAsArray(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingList(): void { @@ -328,6 +341,7 @@ public function testResolvingList(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingNonEmptyList(): void { @@ -352,6 +366,7 @@ public function testResolvingNonEmptyList(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingNullableArray(): void { diff --git a/tests/unit/IntegerRangeResolverTest.php b/tests/unit/IntegerRangeResolverTest.php index 4c7744e..cfe35f6 100644 --- a/tests/unit/IntegerRangeResolverTest.php +++ b/tests/unit/IntegerRangeResolverTest.php @@ -32,6 +32,7 @@ class IntegerRangeResolverTest extends TestCase * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingIntRange(): void { @@ -57,6 +58,7 @@ public function testResolvingIntRange(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingIntRangeWithKeywords(): void { @@ -82,6 +84,7 @@ public function testResolvingIntRangeWithKeywords(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingIntRangeErrorMissingMaxValue(): void { @@ -100,6 +103,7 @@ public function testResolvingIntRangeErrorMissingMaxValue(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingIntRangeErrorMisingMinValue(): void { @@ -118,6 +122,7 @@ public function testResolvingIntRangeErrorMisingMinValue(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingIntRangeErrorMisingComma(): void { @@ -136,6 +141,7 @@ public function testResolvingIntRangeErrorMisingComma(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testResolvingIntRangeErrorMissingEnd(): void { diff --git a/tests/unit/NumericResolverTest.php b/tests/unit/NumericResolverTest.php index d21532c..20284b6 100644 --- a/tests/unit/NumericResolverTest.php +++ b/tests/unit/NumericResolverTest.php @@ -32,7 +32,6 @@ class NumericResolverTest extends TestCase * @uses \phpDocumentor\Reflection\Types\String_ * * @covers ::__construct - * @covers ::resolve */ public function testResolvingIntRange(): void { diff --git a/tests/unit/PseudoTypes/ArrayShapeTest.php b/tests/unit/PseudoTypes/ArrayShapeTest.php index 3a548f4..0069a68 100644 --- a/tests/unit/PseudoTypes/ArrayShapeTest.php +++ b/tests/unit/PseudoTypes/ArrayShapeTest.php @@ -6,6 +6,9 @@ use PHPUnit\Framework\TestCase; +/** + * @coversDefaultClass \phpDocumentor\Reflection\PseudoTypes\ArrayShape + */ class ArrayShapeTest extends TestCase { /** diff --git a/tests/unit/TypeResolverTest.php b/tests/unit/TypeResolverTest.php index eac321d..4e63480 100644 --- a/tests/unit/TypeResolverTest.php +++ b/tests/unit/TypeResolverTest.php @@ -15,6 +15,8 @@ use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; use InvalidArgumentException; +use phpDocumentor\Reflection\PseudoTypes\ArrayShape; +use phpDocumentor\Reflection\PseudoTypes\ArrayShapeItem; use phpDocumentor\Reflection\PseudoTypes\CallableString; use phpDocumentor\Reflection\PseudoTypes\ConstExpression; use phpDocumentor\Reflection\PseudoTypes\False_; @@ -23,6 +25,8 @@ use phpDocumentor\Reflection\PseudoTypes\IntegerRange; use phpDocumentor\Reflection\PseudoTypes\IntegerValue; use phpDocumentor\Reflection\PseudoTypes\List_; +use phpDocumentor\Reflection\PseudoTypes\ListShape; +use phpDocumentor\Reflection\PseudoTypes\ListShapeItem; use phpDocumentor\Reflection\PseudoTypes\LiteralString; use phpDocumentor\Reflection\PseudoTypes\LowercaseString; use phpDocumentor\Reflection\PseudoTypes\NegativeInteger; @@ -31,6 +35,8 @@ use phpDocumentor\Reflection\PseudoTypes\NonEmptyString; use phpDocumentor\Reflection\PseudoTypes\Numeric_; use phpDocumentor\Reflection\PseudoTypes\NumericString; +use phpDocumentor\Reflection\PseudoTypes\ObjectShape; +use phpDocumentor\Reflection\PseudoTypes\ObjectShapeItem; use phpDocumentor\Reflection\PseudoTypes\PositiveInteger; use phpDocumentor\Reflection\PseudoTypes\StringValue; use phpDocumentor\Reflection\PseudoTypes\TraitString; @@ -83,6 +89,7 @@ class TypeResolverTest extends TestCase * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: * * @dataProvider provideKeywords @@ -103,6 +110,7 @@ public function testResolvingKeywords(string $keyword, string $expectedClass): v * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: * * @dataProvider provideClassStrings @@ -127,6 +135,7 @@ public function testResolvingClassStrings(string $classString, bool $throwsExcep * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: * * @dataProvider provideInterfaceStrings @@ -152,6 +161,7 @@ public function testResolvingInterfaceStrings(string $interfaceString, bool $thr * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: * * @dataProvider provideFqcn @@ -175,6 +185,7 @@ public function testResolvingFQSENs(string $fqsen): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingRelativeQSENsBasedOnNamespace(): void @@ -196,6 +207,7 @@ public function testResolvingRelativeQSENsBasedOnNamespace(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingRelativeQSENsBasedOnNamespaceAlias(): void @@ -219,6 +231,7 @@ public function testResolvingRelativeQSENsBasedOnNamespaceAlias(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingTypedArrays(): void @@ -240,6 +253,7 @@ public function testResolvingTypedArrays(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingNullableTypes(): void @@ -260,6 +274,7 @@ public function testResolvingNullableTypes(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingNestedTypedArrays(): void @@ -291,6 +306,7 @@ public function testResolvingNestedTypedArrays(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingCompoundTypes(): void @@ -321,6 +337,7 @@ public function testResolvingCompoundTypes(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingAmpersandCompoundTypes(): void @@ -358,6 +375,7 @@ public function testResolvingAmpersandCompoundTypes(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingMixedCompoundTypes(): void @@ -407,6 +425,7 @@ public function testResolvingMixedCompoundTypes(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingCompoundTypedArrayTypes(): void @@ -438,6 +457,7 @@ public function testResolvingCompoundTypedArrayTypes(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingArrayExpressionObjectsTypes(): void @@ -471,6 +491,7 @@ public function testResolvingArrayExpressionObjectsTypes(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingArrayExpressionSimpleTypes(): void @@ -507,6 +528,7 @@ public function testResolvingArrayExpressionSimpleTypes(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingArrayOfArrayExpressionTypes(): void @@ -542,6 +564,7 @@ public function testResolvingArrayOfArrayExpressionTypes(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testReturnEmptyCompoundOnAnUnclosedArrayExpressionType(): void @@ -561,6 +584,7 @@ public function testReturnEmptyCompoundOnAnUnclosedArrayExpressionType(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingArrayExpressionOrCompoundTypes(): void @@ -602,6 +626,7 @@ public function testResolvingArrayExpressionOrCompoundTypes(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingIterableExpressionSimpleTypes(): void @@ -644,6 +669,7 @@ public function testResolvingIterableExpressionSimpleTypes(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType * @covers :: */ public function testResolvingCompoundTypesWithTwoArrays(): void @@ -719,6 +745,7 @@ public function testAddingAKeywordFailsIfTypeClassDoesNotImplementTypeInterface( * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testExceptionIsThrownIfTypeIsEmpty(): void { @@ -732,6 +759,7 @@ public function testExceptionIsThrownIfTypeIsEmpty(): void * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testInvalidArrayOperator(): void { @@ -839,6 +867,7 @@ public function provideFqcn(): array * * @covers ::__construct * @covers ::resolve + * @covers ::createType */ public function testArrayKeyValueSpecification(): void { @@ -851,10 +880,12 @@ public function testArrayKeyValueSpecification(): void /** * @covers ::__construct * @covers ::resolve + * @covers ::createType * @dataProvider typeProvider * @dataProvider genericsProvider * @dataProvider callableProvider * @dataProvider constExpressions + * @dataProvider shapeStructures * @dataProvider illegalLegacyFormatProvider * @testdox create type from $type */ @@ -1104,6 +1135,42 @@ public function constExpressions(): array ]; } + /** + * @return array + */ + public function shapeStructures(): array + { + return [ + [ + 'array{foo: string, bar: int}', + new ArrayShape( + new ArrayShapeItem('foo', new String_(), false), + new ArrayShapeItem('bar', new Integer(), false) + ), + ], + [ + 'array{foo?: string, bar: int}', + new ArrayShape( + new ArrayShapeItem('foo', new String_(), true), + new ArrayShapeItem('bar', new Integer(), false) + ), + ], + [ + 'object{foo: string, bar: int}', + new ObjectShape( + new ObjectShapeItem('foo', new String_(), false), + new ObjectShapeItem('bar', new Integer(), false) + ), + ], + [ + 'list{1}', + new ListShape( + new ListShapeItem(null, new IntegerValue(1), false) + ), + ], + ]; + } + /** * @return array */