diff --git a/src/Analyzer/Docblock.php b/src/Analyzer/Docblock.php index 63cdd2f1..1c9f63a5 100644 --- a/src/Analyzer/Docblock.php +++ b/src/Analyzer/Docblock.php @@ -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; @@ -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( diff --git a/src/Analyzer/DocblockParserFactory.php b/src/Analyzer/DocblockParserFactory.php index cfabbc3b..abdf2bd2 100644 --- a/src/Analyzer/DocblockParserFactory.php +++ b/src/Analyzer/DocblockParserFactory.php @@ -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); diff --git a/src/Analyzer/DocblockTypesResolver.php b/src/Analyzer/DocblockTypesResolver.php index 498ca707..f36f1b35 100644 --- a/src/Analyzer/DocblockTypesResolver.php +++ b/src/Analyzer/DocblockTypesResolver.php @@ -25,6 +25,8 @@ */ class DocblockTypesResolver extends NodeVisitorAbstract { + public const THROWS_TYPES_ATTRIBUTE = 'docblock_throws_types'; + private NameContext $nameContext; private bool $parseCustomAnnotations; @@ -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 @@ -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); } /** diff --git a/src/Analyzer/FileVisitor.php b/src/Analyzer/FileVisitor.php index 7ffedda9..72c0e1f9 100644 --- a/src/Analyzer/FileVisitor.php +++ b/src/Analyzer/FileVisitor.php @@ -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 @@ -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) { diff --git a/tests/Unit/Analyzer/DocblockParserTest.php b/tests/Unit/Analyzer/DocblockParserTest.php index 163be91e..c72bd584 100644 --- a/tests/Unit/Analyzer/DocblockParserTest.php +++ b/tests/Unit/Analyzer/DocblockParserTest.php @@ -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(); diff --git a/tests/Unit/Analyzer/DocblockTypesResolverTest.php b/tests/Unit/Analyzer/DocblockTypesResolverTest.php index c80067f4..7b7db4d4 100644 --- a/tests/Unit/Analyzer/DocblockTypesResolverTest.php +++ b/tests/Unit/Analyzer/DocblockTypesResolverTest.php @@ -66,8 +66,14 @@ public function myMethod(array $users, array $products, MyOtherClass $other): vo * @param array $users * * @return array + * + * @throws \Exception + * @throws \Domain\Foo\FooException + * @throws BarException */ public function myMethod2(array $aParam, array $users): array + { + } } EOF; @@ -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()); } }