diff --git a/README.md b/README.md index 4ee17ee..eef9481 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ This library is an alternative to [native PHP Reflection](https://www.php.net/ma composer require typhoon/reflection jetbrains/phpstorm-stubs ``` -Installing `jetbrains/phpstorm-stubs` is highly recommended. -Without stubs native PHP classes are reflected via native reflector that does not support templates. +Installing `jetbrains/phpstorm-stubs` is highly recommended. Without stubs core PHP classes are reflected via +[NativeReflector](src/NativeReflector/NativeReflector.php) that does not support phpDoc types. ## Basic Usage diff --git a/src/Metadata/MethodMetadata.php b/src/Metadata/MethodMetadata.php index c52edea..a90d207 100644 --- a/src/Metadata/MethodMetadata.php +++ b/src/Metadata/MethodMetadata.php @@ -5,6 +5,8 @@ namespace Typhoon\Reflection\Metadata; use Typhoon\Reflection\TemplateReflection; +use Typhoon\Type\Type; +use Typhoon\Type\types; /** * @internal @@ -48,6 +50,7 @@ public function __construct( public readonly bool $returnsReference = false, public readonly bool $generator = false, public readonly bool $deprecated = false, + public readonly Type $throwsType = types::never, public readonly array $attributes = [], ) {} diff --git a/src/MethodReflection.php b/src/MethodReflection.php index d100293..aa31ede 100644 --- a/src/MethodReflection.php +++ b/src/MethodReflection.php @@ -291,6 +291,11 @@ public function getTyphoonReturnType(Origin $origin = Origin::Resolved): ?Type return $this->metadata->returnType->get($origin); } + public function getTyphoonThrowsType(): Type + { + return $this->metadata->throwsType; + } + public function hasPrototype(): bool { return $this->metadata->prototype !== null; diff --git a/src/PhpDocParser/PhpDoc.php b/src/PhpDocParser/PhpDoc.php index 0c54e07..c668433 100644 --- a/src/PhpDocParser/PhpDoc.php +++ b/src/PhpDocParser/PhpDoc.php @@ -12,6 +12,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode; @@ -39,6 +40,11 @@ final class PhpDoc private null|TypeNode|false $returnType = false; + /** + * @var ?list + */ + private ?array $throwsTypes = null; + /** * @var ?list */ @@ -211,6 +217,28 @@ public function returnType(): ?TypeNode return $this->returnType = $returnTag?->value->type; } + /** + * @return list + */ + public function throwsTypes(): array + { + if ($this->throwsTypes !== null) { + return $this->throwsTypes; + } + + $throwsTypes = []; + + foreach ($this->tags as $tag) { + if (!$tag->value instanceof ThrowsTagValueNode) { + continue; + } + + $throwsTypes[] = $tag->value->type; + } + + return $this->throwsTypes = $throwsTypes; + } + /** * @return list */ diff --git a/src/PhpParserReflector/ContextualPhpParserReflector.php b/src/PhpParserReflector/ContextualPhpParserReflector.php index 831507e..0904b48 100644 --- a/src/PhpParserReflector/ContextualPhpParserReflector.php +++ b/src/PhpParserReflector/ContextualPhpParserReflector.php @@ -10,6 +10,7 @@ use PhpParser\Node\Stmt; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use Typhoon\Reflection\FileResource; use Typhoon\Reflection\Metadata\AttributeMetadata; use Typhoon\Reflection\Metadata\ClassMetadata; @@ -383,6 +384,7 @@ class: $class, returnsReference: $node->byRef, generator: MethodReflections::isGenerator($node), deprecated: $phpDoc->isDeprecated(), + throwsType: $this->reflectThrowsType($phpDoc->throwsTypes()), attributes: $this->reflectAttributes($node->attrGroups, \Attribute::TARGET_METHOD), )); } @@ -445,6 +447,34 @@ private function reflectType(?Node $native = null, ?TypeNode $phpDoc = null, boo ); } + /** + * @param list $throwsTypes + */ + private function reflectThrowsType(array $throwsTypes): Type\Type + { + if ($throwsTypes === []) { + return types::never; + } + + if (\count($throwsTypes) === 1) { + return $this->phpDocTypeReflector->reflect($throwsTypes[0]); + } + + $flatTypes = []; + + foreach ($throwsTypes as $throwsType) { + if ($throwsType instanceof UnionTypeNode) { + foreach ($throwsType->types as $type) { + $flatTypes[] = $type; + } + } else { + $flatTypes[] = $throwsType; + } + } + + return $this->phpDocTypeReflector->reflect(new UnionTypeNode($flatTypes)); + } + /** * @return list */ diff --git a/src/PhpParserReflector/EnumReflections.php b/src/PhpParserReflector/EnumReflections.php index 1c380a5..6ec0cf0 100644 --- a/src/PhpParserReflector/EnumReflections.php +++ b/src/PhpParserReflector/EnumReflections.php @@ -78,6 +78,7 @@ functionOrMethod: 'from', ], returnType: TypeMetadata::create(types::array(), types::list(types::object($class))), internal: true, + throwsType: types::object(\ValueError::class), ); } diff --git a/tests/unit/PhpDocParser/PhpDocAndParserTest.php b/tests/unit/PhpDocParser/PhpDocAndParserTest.php index 54cf1c9..437cf78 100644 --- a/tests/unit/PhpDocParser/PhpDocAndParserTest.php +++ b/tests/unit/PhpDocParser/PhpDocAndParserTest.php @@ -10,6 +10,7 @@ use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Typhoon\Reflection\Variance; @@ -243,6 +244,33 @@ public function testItReturnsLatestPrioritizedReturnTagType(): void self::assertEquals(new IdentifierTypeNode('string'), $returnType); } + public function testItReturnsAllThrowsTypes(): void + { + $parser = new PhpDocParser(); + + $throwsTypes = $parser->parsePhpDoc( + <<<'PHP' + /** + * @throws RuntimeException|LogicException + * @throws \Exception + * @phpstan-throws \OutOfBoundsException + */ + PHP, + )->throwsTypes(); + + self::assertEquals( + [ + new UnionTypeNode([ + new IdentifierTypeNode('RuntimeException'), + new IdentifierTypeNode('LogicException'), + ]), + new IdentifierTypeNode('\Exception'), + new IdentifierTypeNode('\OutOfBoundsException'), + ], + $throwsTypes, + ); + } + public function testItReturnsEmptyTemplatesWhenNoTemplateTag(): void { $parser = new PhpDocParser(); @@ -551,6 +579,8 @@ public function testMethodsMemoized(): void * @return array * @phpstan-type A int * @phpstan-import-type C from bool as A + * @phpstan-throws RuntimeException + * @throws LogicException */ PHP, ); @@ -564,6 +594,7 @@ public function testMethodsMemoized(): void $phpDoc->returnType(), $phpDoc->typeAliases(), $phpDoc->typeAliasImports(), + $phpDoc->throwsTypes(), ]; $first = $tags();