Skip to content

Collect dependencies defined in @throws docblock tags #513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/Analyzer/Docblock.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
Expand Down Expand Up @@ -43,6 +44,17 @@ public function getReturnTagTypes(): array
return array_filter($returnTypes);
}

public function getThrowTagsTypes(): array
{
$throwTypes = array_map(
fn (ThrowsTagValueNode $throwTag) => $this->getType($throwTag->type),
$this->phpDocNode->getThrowsTagValues()
);

// remove null values
return array_filter($throwTypes);
}

public function getVarTagTypes(): array
{
$varTypes = array_map(
Expand Down
2 changes: 1 addition & 1 deletion src/Analyzer/DocblockParserFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static function create(): DocblockParser

// this if is to allow using v 1.2 or v2
if (class_exists(ParserConfig::class)) {
$parserConfig = new ParserConfig([]);
$parserConfig = new ParserConfig(['lines' => true]);
$constExprParser = new ConstExprParser($parserConfig);
$typeParser = new TypeParser($parserConfig, $constExprParser);
$phpDocParser = new PhpDocParser($parserConfig, $typeParser, $constExprParser);
Expand Down
70 changes: 62 additions & 8 deletions src/Analyzer/DocblockTypesResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
*/
class DocblockTypesResolver extends NodeVisitorAbstract
{
public const THROWS_TYPES_ATTRIBUTE = 'docblock_throws_types';

private NameContext $nameContext;

private bool $parseCustomAnnotations;
Expand Down Expand Up @@ -117,6 +119,18 @@ private function resolveFunctionTypes(Node $node): void
return;
}

$this->resolveParamTypes($node, $docblock);

$this->resolveReturnValueType($node, $docblock);

$this->resolveThrowsValueType($node, $docblock);
}

/**
* @param Stmt\ClassMethod|Stmt\Function_|Expr\Closure|Expr\ArrowFunction $node
*/
private function resolveParamTypes(Node $node, Docblock $docblock): void
{
// extract param types from param tags
foreach ($node->params as $param) {
if (!$this->isTypeArray($param->type)) { // not an array, nothing to do
Expand All @@ -136,19 +150,59 @@ private function resolveFunctionTypes(Node $node): void

$param->type = $this->resolveName(new Name($type), Stmt\Use_::TYPE_NORMAL);
}
}

// extract return type from return tag
if ($this->isTypeArray($node->returnType)) {
$type = $docblock->getReturnTagTypes();
$type = array_pop($type);
/**
* @param Stmt\ClassMethod|Stmt\Function_|Expr\Closure|Expr\ArrowFunction $node
*/
private function resolveReturnValueType(Node $node, Docblock $docblock): void
{
if (null === $node->returnType) {
return;
}

// we ignore any type which is not a class
if (!$this->isTypeClass($type)) {
return;
if (!$this->isTypeArray($node->returnType)) {
return;
}

$type = $docblock->getReturnTagTypes();
$type = array_pop($type);

// we ignore any type which is not a class
if (!$this->isTypeClass($type)) {
return;
}

$node->returnType = $this->resolveName(new Name($type), Stmt\Use_::TYPE_NORMAL);
}

/**
* @param Stmt\ClassMethod|Stmt\Function_|Expr\Closure|Expr\ArrowFunction $node
*/
private function resolveThrowsValueType(Node $node, Docblock $docblock): void
{
// extract throw types from throw tag
$throwValues = $docblock->getThrowTagsTypes();

if (empty($throwValues)) {
return;
}

$throwsTypesResolved = [];

foreach ($throwValues as $throwValue) {
if (str_starts_with($throwValue, '\\')) {
$name = new FullyQualified(substr($throwValue, 1));
} else {
$name = $this->resolveName(new Name($throwValue), Stmt\Use_::TYPE_NORMAL);
}

$node->returnType = $this->resolveName(new Name($type), Stmt\Use_::TYPE_NORMAL);
$name->setAttribute('startLine', $node->getStartLine());

$throwsTypesResolved[] = $name;
}

$node->setAttribute(self::THROWS_TYPES_ATTRIBUTE, $throwsTypesResolved);
}

/**
Expand Down
16 changes: 16 additions & 0 deletions src/Analyzer/FileVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public function enterNode(Node $node): void

// handles attribute definition like #[MyAttribute]
$this->handleAttributeNode($node);

// handles throws types like @throws MyClass
$this->handleThrowsTags($node);
}

public function getClassDescriptions(): array
Expand Down Expand Up @@ -334,6 +337,19 @@ private function handleAttributeNode(Node $node): void
->addAttribute($node->name->toString(), $node->getLine());
}

private function handleThrowsTags(Node $node): void
{
if (!$node->hasAttribute(DocblockTypesResolver::THROWS_TYPES_ATTRIBUTE)) {
return;
}

/** @var Node\Name\FullyQualified $throw */
foreach ($node->getAttribute(DocblockTypesResolver::THROWS_TYPES_ATTRIBUTE) as $throw) {
$this->classDescriptionBuilder
->addDependency(new ClassDependency($throw->toString(), $throw->getLine()));
}
}

private function addParamDependency(Node\Param $node): void
{
if (null === $node->type || $node->type instanceof Node\Identifier) {
Expand Down
21 changes: 21 additions & 0 deletions tests/Unit/Analyzer/DocblockParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,27 @@ public function test_it_should_extract_types_from_var_tag(): void
self::assertEquals('(int | string)', $varTags[6]);
}

public function test_it_should_extract_types_from_throws_tag(): void
{
$parser = DocblockParserFactory::create();

$code = <<< 'PHP'
/**
* @throws \Exception
* @throws \Domain\Foo\FooException
* @throws BarException
*/
PHP;

$db = $parser->parse($code);

$varTags = $db->getThrowTagsTypes();
self::assertCount(3, $varTags);
self::assertEquals('\Exception', $varTags[0]);
self::assertEquals('\Domain\Foo\FooException', $varTags[1]);
self::assertEquals('BarException', $varTags[2]);
}

public function test_it_should_extract_doctrine_like_annotations(): void
{
$parser = DocblockParserFactory::create();
Expand Down
15 changes: 13 additions & 2 deletions tests/Unit/Analyzer/DocblockTypesResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,14 @@ public function myMethod(array $users, array $products, MyOtherClass $other): vo
* @param array<int, User> $users
*
* @return array<int, int|string>
*
* @throws \Exception
* @throws \Domain\Foo\FooException
* @throws BarException
*/
public function myMethod2(array $aParam, array $users): array
{
}
}
EOF;

Expand All @@ -76,13 +82,18 @@ public function myMethod2(array $aParam, array $users): array
$cd = $parser->getClassDescriptions()[0];
$dep = $cd->getDependencies();

self::assertCount(7, $cd->getDependencies());
self::assertCount(10, $cd->getDependencies());
self::assertEquals('Application\Model\User', $dep[0]->getFQCN()->toString());
self::assertEquals('Application\MyDto', $dep[1]->getFQCN()->toString());
self::assertEquals('Domain\ValueObject', $dep[2]->getFQCN()->toString());
self::assertEquals('Application\Model\User', $dep[3]->getFQCN()->toString());
self::assertEquals('Application\Model\Product', $dep[4]->getFQCN()->toString());
self::assertEquals('Domain\Foo\MyOtherClass', $dep[5]->getFQCN()->toString());
self::assertEquals('Application\Model\User', $dep[6]->getFQCN()->toString());
self::assertEquals('Exception', $dep[6]->getFQCN()->toString());
self::assertEquals('Domain\Foo\FooException', $dep[7]->getFQCN()->toString());
self::assertEquals('Domain\Foo\BarException', $dep[8]->getFQCN()->toString());

self::assertEquals('Application\Model\User', $dep[9]->getFQCN()->toString());
self::assertEquals(46, $dep[9]->getLine());
}
}