Skip to content

Commit

Permalink
Added support for @throws in methods
Browse files Browse the repository at this point in the history
Closes #16
  • Loading branch information
vudaltsov committed Feb 21, 2024
1 parent 3e48f9e commit 5e8d9d8
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 2 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions src/Metadata/MethodMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace Typhoon\Reflection\Metadata;

use Typhoon\Reflection\TemplateReflection;
use Typhoon\Type\Type;
use Typhoon\Type\types;

/**
* @internal
Expand Down Expand Up @@ -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 = [],
) {}

Expand Down
5 changes: 5 additions & 0 deletions src/MethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions src/PhpDocParser/PhpDoc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,6 +40,11 @@ final class PhpDoc

private null|TypeNode|false $returnType = false;

/**
* @var ?list<TypeNode>
*/
private ?array $throwsTypes = null;

/**
* @var ?list<TemplateTagValueNode>
*/
Expand Down Expand Up @@ -211,6 +217,28 @@ public function returnType(): ?TypeNode
return $this->returnType = $returnTag?->value->type;
}

/**
* @return list<TypeNode>
*/
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<TemplateTagValueNode>
*/
Expand Down
30 changes: 30 additions & 0 deletions src/PhpParserReflector/ContextualPhpParserReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
));
}
Expand Down Expand Up @@ -445,6 +447,34 @@ private function reflectType(?Node $native = null, ?TypeNode $phpDoc = null, boo
);
}

/**
* @param list<TypeNode> $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<TemplateReflection>
*/
Expand Down
1 change: 1 addition & 0 deletions src/PhpParserReflector/EnumReflections.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ functionOrMethod: 'from',
],
returnType: TypeMetadata::create(types::array(), types::list(types::object($class))),
internal: true,
throwsType: types::object(\ValueError::class),
);
}

Expand Down
31 changes: 31 additions & 0 deletions tests/unit/PhpDocParser/PhpDocAndParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
);
Expand All @@ -564,6 +594,7 @@ public function testMethodsMemoized(): void
$phpDoc->returnType(),
$phpDoc->typeAliases(),
$phpDoc->typeAliasImports(),
$phpDoc->throwsTypes(),
];

$first = $tags();
Expand Down

0 comments on commit 5e8d9d8

Please sign in to comment.