From c92bb852fd57093641c99cf7705ba83ea00d3a38 Mon Sep 17 00:00:00 2001 From: Jaapio Date: Fri, 22 Mar 2024 11:08:13 +0100 Subject: [PATCH 1/2] Add support for object and list shape types --- src/PseudoTypes/ArrayShapeItem.php | 49 +---------------- src/PseudoTypes/ListShape.php | 16 ++++++ src/PseudoTypes/ListShapeItem.php | 9 +++ src/PseudoTypes/ObjectShape.php | 41 ++++++++++++++ src/PseudoTypes/ObjectShapeItem.php | 9 +++ src/PseudoTypes/ShapeItem.php | 56 +++++++++++++++++++ src/TypeResolver.php | 45 ++++++++++++++- tests/unit/CollectionResolverTest.php | 15 +++++ tests/unit/IntegerRangeResolverTest.php | 6 ++ tests/unit/NumericResolverTest.php | 1 - tests/unit/PseudoTypes/ArrayShapeTest.php | 3 + tests/unit/TypeResolverTest.php | 67 +++++++++++++++++++++++ 12 files changed, 265 insertions(+), 52 deletions(-) create mode 100644 src/PseudoTypes/ListShape.php create mode 100644 src/PseudoTypes/ListShapeItem.php create mode 100644 src/PseudoTypes/ObjectShape.php create mode 100644 src/PseudoTypes/ObjectShapeItem.php create mode 100644 src/PseudoTypes/ShapeItem.php 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..deef3ab --- /dev/null +++ b/src/PseudoTypes/ListShape.php @@ -0,0 +1,16 @@ +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 */ From b6f5321852fcc1b1a50e3658cef71421ba4c806e Mon Sep 17 00:00:00 2001 From: Jaapio Date: Fri, 22 Mar 2024 12:00:35 +0100 Subject: [PATCH 2/2] Bump phpstan/phpdoc-parser to support object shapes --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index c4afb7d..13045ef 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "require": { "php": "^7.3 || ^8.0", "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.13", + "phpstan/phpdoc-parser": "^1.18", "doctrine/deprecations": "^1.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index ce070db..eec91a0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6db1e320c0ec65669131eb815d876ec5", + "content-hash": "2d7307b14c95f0d2eb310a28ae73dae0", "packages": [ { "name": "doctrine/deprecations", @@ -108,16 +108,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.25.0", + "version": "1.27.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" + "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/86e4d5a4b036f8f0be1464522f4c6b584c452757", + "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757", "shasum": "" }, "require": { @@ -149,9 +149,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.27.0" }, - "time": "2024-01-04T17:06:16+00:00" + "time": "2024-03-21T13:14:53+00:00" } ], "packages-dev": [