diff --git a/composer.json b/composer.json index 1881f79af81a..1c4e88a82d05 100644 --- a/composer.json +++ b/composer.json @@ -212,6 +212,7 @@ ], "files": [ "packages/better-php-doc-parser/tests/PhpDocInfo/PhpDocInfoPrinter/AbstractPhpDocInfoPrinterTest.php", + "rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Source/i_throw_an_exception.func.php", "rules/dead-code/tests/Rector/MethodCall/RemoveDefaultArgumentValueRector/Source/UserDefined.php", "rules/type-declaration/tests/Rector/Property/CompleteVarDocTypePropertyRector/Source/EventDispatcher.php", "rules/type-declaration/tests/Rector/FunctionLike/ReturnTypeDeclarationRector/Source/MyBar.php", diff --git a/rules/coding-style/src/Rector/Throw_/AnnotateThrowablesRector.php b/rules/coding-style/src/Rector/Throw_/AnnotateThrowablesRector.php index a7be31889ca3..7648f7bb6f90 100644 --- a/rules/coding-style/src/Rector/Throw_/AnnotateThrowablesRector.php +++ b/rules/coding-style/src/Rector/Throw_/AnnotateThrowablesRector.php @@ -4,26 +4,29 @@ namespace Rector\CodingStyle\Rector\Throw_; -use Nette\Utils\Reflection; -use Nette\Utils\Strings; use PhpParser\Node; +use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Identifier; use PhpParser\Node\Name\FullyQualified; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\Throw_; use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; -use PHPStan\Type\ObjectType; use Rector\AttributeAwarePhpDoc\Ast\PhpDoc\AttributeAwarePhpDocTagNode; -use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; use Rector\Core\Exception\ShouldNotHappenException; +use Rector\Core\PhpDoc\PhpDocTagsFinder; +use Rector\Core\PhpParser\Node\Value\ClassResolver; use Rector\Core\Rector\AbstractRector; use Rector\Core\RectorDefinition\CodeSample; use Rector\Core\RectorDefinition\RectorDefinition; +use Rector\Core\Reflection\ClassMethodReflectionHelper; +use Rector\Core\Reflection\FunctionReflectionHelper; use Rector\NodeTypeResolver\Node\AttributeKey; -use Rector\PHPStan\Type\ShortenedObjectType; -use ReflectionMethod; +use ReflectionFunction; /** * @see \Rector\CodingStyle\Tests\Rector\Throw_\AnnotateThrowablesRector\AnnotateThrowablesRectorTest @@ -31,21 +34,48 @@ final class AnnotateThrowablesRector extends AbstractRector { /** - * @var string + * @var array */ - private const RETURN_DOCBLOCK_TAG_REGEX = '#@return[ a-zA-Z0-9\|\\\t]+#'; + private $throwablesToAnnotate = []; /** - * @var array + * @var FunctionReflectionHelper + */ + private $functionReflectionHelper; + + /** + * @var ClassMethodReflectionHelper + */ + private $classMethodReflectionHelper; + + /** + * @var ClassResolver */ - private $foundThrownClasses = []; + private $classResolver; + + /** + * @var PhpDocTagsFinder + */ + private $phpDocTagsFinder; + + public function __construct( + ClassMethodReflectionHelper $classMethodReflectionHelper, + ClassResolver $classResolver, + FunctionReflectionHelper $functionReflectionHelper, + PhpDocTagsFinder $phpDocTagsFinder + ) { + $this->functionReflectionHelper = $functionReflectionHelper; + $this->classMethodReflectionHelper = $classMethodReflectionHelper; + $this->classResolver = $classResolver; + $this->phpDocTagsFinder = $phpDocTagsFinder; + } /** * @return string[] */ public function getNodeTypes(): array { - return [Throw_::class]; + return [Throw_::class, FuncCall::class, MethodCall::class]; } /** @@ -94,162 +124,186 @@ public function throwException(int $code) } /** - * @param Throw_ $node + * @param Node|Throw_|MethodCall|FuncCall $node */ public function refactor(Node $node): ?Node { - if ($this->isThrowableAnnotated($node)) { - return null; + $this->throwablesToAnnotate = []; + if ($this->hasThrowablesToAnnotate($node)) { + $this->annotateThrowables($node); + return $node; } - $this->annotateThrowable($node); - - return $node; + return null; } - private function isThrowableAnnotated(Throw_ $throw): bool + private function hasThrowablesToAnnotate(Node $node): bool { - $phpDocInfo = $this->getThrowingStmtPhpDocInfo($throw); - $identifiedThrownThrowables = $this->identifyThrownThrowables($throw); + $foundThrowables = 0; + if ($node instanceof Throw_) { + $foundThrowables = $this->analyzeStmtThrow($node); + } - foreach ($phpDocInfo->getThrowsTypes() as $throwsType) { - if (! $throwsType instanceof ObjectType) { - continue; - } + if ($node instanceof FuncCall) { + $foundThrowables = $this->analyzeStmtFuncCall($node); + } - $className = $throwsType instanceof ShortenedObjectType - ? $throwsType->getFullyQualifiedName() - : $throwsType->getClassName(); + if ($node instanceof MethodCall) { + $foundThrowables = $this->analyzeStmtMethodCall($node); + } - if (! in_array($className, $identifiedThrownThrowables, true)) { - continue; - } + return $foundThrowables > 0; + } - return true; + private function annotateThrowables(Node $node): void + { + $callee = $this->identifyCallee($node); + + if ($callee === null) { + return; } - return false; + $phpDocInfo = $callee->getAttribute(AttributeKey::PHP_DOC_INFO); + foreach ($this->throwablesToAnnotate as $throwableToAnnotate) { + $docComment = $this->buildThrowsDocComment($throwableToAnnotate); + $phpDocInfo->addPhpDocTagNode($docComment); + } } - private function identifyThrownThrowables(Throw_ $throw): array + private function analyzeStmtThrow(Throw_ $throw): int { + $foundThrownThrowables = []; + + // throw new \Throwable if ($throw->expr instanceof New_) { - return [$this->getName($throw->expr->class)]; + $class = $this->getName($throw->expr->class); + if ($class !== null) { + $foundThrownThrowables[] = $class; + } } if ($throw->expr instanceof StaticCall) { - return $this->identifyThrownThrowablesInStaticCall($throw->expr); + $foundThrownThrowables = $this->identifyThrownThrowablesInStaticCall($throw->expr); } if ($throw->expr instanceof MethodCall) { - return $this->identifyThrownThrowablesInMethodCall($throw->expr); + $foundThrownThrowables = $this->identifyThrownThrowablesInMethodCall($throw->expr); } - return []; + $alreadyAnnotatedThrowables = $this->extractAlreadyAnnotatedThrowables($throw); + + return $this->diffThrowables($foundThrownThrowables, $alreadyAnnotatedThrowables); } - private function identifyThrownThrowablesInMethodCall(MethodCall $methodCall): array + private function analyzeStmtFuncCall(FuncCall $funcCall): int { - $thrownClass = $methodCall->var - ->getAttribute(AttributeKey::FUNCTION_NODE)->name - ->getAttribute('nextNode')->expr->var - ->getAttribute('nextNode')->class; + $functionFqn = $this->getName($funcCall); - if (! $thrownClass instanceof FullyQualified) { - throw new ShouldNotHappenException(); + if ($functionFqn === null) { + return 0; } - $classFqn = implode('\\', $thrownClass->parts); - $methodNode = $methodCall->var->getAttribute('nextNode'); - $methodName = $methodNode->name; + $reflectedFunction = new ReflectionFunction($functionFqn); + $foundThrownThrowables = $this->functionReflectionHelper->extractFunctionAnnotatedThrows($reflectedFunction); + $alreadyAnnotatedThrowables = $this->extractAlreadyAnnotatedThrowables($funcCall); + return $this->diffThrowables($foundThrownThrowables, $alreadyAnnotatedThrowables); + } + + private function analyzeStmtMethodCall(MethodCall $methodCall): int + { + $foundThrownThrowables = $this->identifyThrownThrowablesInMethodCall($methodCall); + $alreadyAnnotatedThrowables = $this->extractAlreadyAnnotatedThrowables($methodCall); + return $this->diffThrowables($foundThrownThrowables, $alreadyAnnotatedThrowables); + } + + private function identifyThrownThrowablesInMethodCall(MethodCall $methodCall): array + { + $methodClass = $this->classResolver->getClassFromMethodCall($methodCall); + $methodName = $methodCall->name; + + if (! $methodClass instanceof FullyQualified || ! $methodName instanceof Identifier) { + return []; + } - return $this->extractMethodReturnsFromDocblock($classFqn, $methodName); + return $methodCall->getAttribute('parentNode') instanceof Throw_ + ? $this->extractMethodReturns($methodClass, $methodName) + : $this->extractMethodThrows($methodClass, $methodName); } private function identifyThrownThrowablesInStaticCall(StaticCall $staticCall): array { $thrownClass = $staticCall->class; + $methodName = $thrownClass->getAttribute('nextNode'); - if (! $thrownClass instanceof FullyQualified) { + if (! $thrownClass instanceof FullyQualified || ! $methodName instanceof Identifier) { throw new ShouldNotHappenException(); } - $classFqn = implode('\\', $thrownClass->parts); - $methodNode = $thrownClass->getAttribute('nextNode'); - $methodName = $methodNode->name; - return $this->extractMethodReturnsFromDocblock($classFqn, $methodName); + return $this->extractMethodReturns($thrownClass, $methodName); } - private function extractMethodReturnsFromDocblock(string $classFqn, string $methodName): array + private function extractMethodReturns(FullyQualified $fullyQualified, Identifier $identifier): array { - $reflectedMethod = new ReflectionMethod($classFqn, $methodName); - $methodDocblock = $reflectedMethod->getDocComment(); + $method = $identifier->name; + $class = $this->getName($fullyQualified); - // copied from https://github.com/nette/di/blob/d1c0598fdecef6d3b01e2ace5f2c30214b3108e6/src/DI/Autowiring.php#L215 - $result = Strings::match((string) $methodDocblock, self::RETURN_DOCBLOCK_TAG_REGEX); - if ($result === null) { + if ($class === null) { return []; } - $returnTags = explode('|', str_replace('@return ', '', $result[0])); - $returnClasses = []; - foreach ($returnTags as $returnTag) { - $returnClasses[] = Reflection::expandClassName($returnTag, $reflectedMethod->getDeclaringClass()); - } - - $this->foundThrownClasses = $returnClasses; - - return $returnClasses; + return $this->classMethodReflectionHelper->extractTagsFromMethodDockblock($class, $method, '@return'); } - private function annotateThrowable(Throw_ $node): void + private function extractMethodThrows(FullyQualified $fullyQualified, Identifier $identifier): array { - $throwClass = $this->buildFQN($node); - if ($throwClass !== null) { - $this->foundThrownClasses[] = $throwClass; - } - - if (empty($this->foundThrownClasses)) { - return; - } + $method = $identifier->name; + $class = $this->getName($fullyQualified); - foreach ($this->foundThrownClasses as $thrownClass) { - $docComment = $this->buildThrowsDocComment($thrownClass); - - $throwingStmtPhpDocInfo = $this->getThrowingStmtPhpDocInfo($node); - $throwingStmtPhpDocInfo->addPhpDocTagNode($docComment); + if ($class === null) { + return []; } - $this->foundThrownClasses = []; + return $this->classMethodReflectionHelper->extractTagsFromMethodDockblock($class, $method, '@throws'); } - private function buildThrowsDocComment(string $throwableClass): AttributeAwarePhpDocTagNode + private function extractAlreadyAnnotatedThrowables(Node $node): array { - $genericTagValueNode = new ThrowsTagValueNode(new IdentifierTypeNode('\\' . $throwableClass), ''); + $callee = $this->identifyCallee($node); - return new AttributeAwarePhpDocTagNode('@throws', $genericTagValueNode); + return $callee === null ? [] : $this->phpDocTagsFinder->extractTagsThrowsFromNode($callee); } - private function buildFQN(Throw_ $throw): ?string + private function diffThrowables(array $foundThrownThrowables, array $alreadyAnnotatedThrowables): int { - if (! $throw->expr instanceof New_) { - return null; - } + $normalizeNamespace = static function (string $class): string { + $class = ltrim($class, '\\'); + return '\\' . $class; + }; + + $foundThrownThrowables = array_map($normalizeNamespace, $foundThrownThrowables); + $alreadyAnnotatedThrowables = array_map($normalizeNamespace, $alreadyAnnotatedThrowables); + + $filterClasses = static function (string $class): bool { + return class_exists($class) || interface_exists($class); + }; + + $foundThrownThrowables = array_filter($foundThrownThrowables, $filterClasses); + $alreadyAnnotatedThrowables = array_filter($alreadyAnnotatedThrowables, $filterClasses); - return $this->getName($throw->expr->class); + $this->throwablesToAnnotate = array_diff($foundThrownThrowables, $alreadyAnnotatedThrowables); + + return count($this->throwablesToAnnotate); } - private function getThrowingStmtPhpDocInfo(Throw_ $throw): PhpDocInfo + private function buildThrowsDocComment(string $throwableClass): AttributeAwarePhpDocTagNode { - $method = $throw->getAttribute(AttributeKey::METHOD_NODE); - $function = $throw->getAttribute(AttributeKey::FUNCTION_NODE); + $genericTagValueNode = new ThrowsTagValueNode(new IdentifierTypeNode($throwableClass), ''); - /** @var Node|null $stmt */ - $stmt = $method ?? $function ?? null; - if ($stmt === null) { - throw new ShouldNotHappenException(); - } + return new AttributeAwarePhpDocTagNode('@throws', $genericTagValueNode); + } - return $stmt->getAttribute(AttributeKey::PHP_DOC_INFO); + private function identifyCallee(Node $node): ?Node + { + return $this->betterNodeFinder->findFirstAncestorInstancesOf($node, [ClassMethod::class, Function_::class]); } } diff --git a/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_function_that_may_throw_an_exception.php.inc b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_function_that_may_throw_an_exception.php.inc new file mode 100644 index 000000000000..7488d9396578 --- /dev/null +++ b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_function_that_may_throw_an_exception.php.inc @@ -0,0 +1,36 @@ + +----- + diff --git a/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_function_that_may_throw_an_exception_already_annotated.php.inc b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_function_that_may_throw_an_exception_already_annotated.php.inc new file mode 100644 index 000000000000..d24007cb163b --- /dev/null +++ b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_function_that_may_throw_an_exception_already_annotated.php.inc @@ -0,0 +1,42 @@ + +----- + diff --git a/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_method_of_a_class_passed_as_parameter.php.inc b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_method_of_a_class_passed_as_parameter.php.inc new file mode 100644 index 000000000000..0b1377f21bbc --- /dev/null +++ b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_method_of_a_class_passed_as_parameter.php.inc @@ -0,0 +1,60 @@ +thisMethodThrowsAnException(); + } +} + +?> +----- +thisMethodThrowsAnException(); + } +} + +?> diff --git a/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_method_that_may_throw_an_exception.php.inc b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_method_that_may_throw_an_exception.php.inc new file mode 100644 index 000000000000..0dc3f114a799 --- /dev/null +++ b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_method_that_may_throw_an_exception.php.inc @@ -0,0 +1,41 @@ +mayThrowAnException(1); + } +} + +?> +----- +mayThrowAnException(1); + } +} + +?> diff --git a/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_method_that_may_throw_an_exception_already_annotated.php.inc b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_method_that_may_throw_an_exception_already_annotated.php.inc new file mode 100644 index 000000000000..f908c65871b1 --- /dev/null +++ b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_a_method_that_may_throw_an_exception_already_annotated.php.inc @@ -0,0 +1,42 @@ +mayThrowAnException(1); + } +} + +?> +----- +mayThrowAnException(1); + } +} + +?> diff --git a/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_this.php.inc b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_this.php.inc new file mode 100644 index 000000000000..03e14efa7225 --- /dev/null +++ b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Fixture/use_of_this.php.inc @@ -0,0 +1,56 @@ +thisMethodThrowsAnException(); + } +} + +?> +----- +thisMethodThrowsAnException(); + } +} + +?> diff --git a/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/FixtureTodo/factory_method_no_return_type_hinting.php.inc b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/FixtureTodo/factory_method_no_return_type_hinting.php.inc deleted file mode 100644 index 7313b3517bbb..000000000000 --- a/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/FixtureTodo/factory_method_no_return_type_hinting.php.inc +++ /dev/null @@ -1,11 +0,0 @@ -createException(1); -} diff --git a/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Source/MethodThatMayThrowAnException.php b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Source/MethodThatMayThrowAnException.php new file mode 100644 index 000000000000..9a1d5377182b --- /dev/null +++ b/rules/coding-style/tests/Rector/Throw_/AnnotateThrowablesRector/Source/MethodThatMayThrowAnException.php @@ -0,0 +1,28 @@ +getAttribute(AttributeKey::PHP_DOC_INFO); + foreach ($phpDocInfo->getThrowsTypes() as $throwsType) { + $thrownClass = null; + if ($throwsType instanceof ShortenedObjectType) { + $thrownClass = $throwsType->getFullyQualifiedName(); + } + + if ($throwsType instanceof FullyQualifiedObjectType) { + $thrownClass = $throwsType->getClassName(); + } + + if ($thrownClass !== null) { + $throwsTags[] = $thrownClass; + } + } + + return $throwsTags; + } +} diff --git a/src/PhpParser/Node/Value/ClassResolver.php b/src/PhpParser/Node/Value/ClassResolver.php new file mode 100644 index 000000000000..8c124c6e2ab9 --- /dev/null +++ b/src/PhpParser/Node/Value/ClassResolver.php @@ -0,0 +1,103 @@ +getAttribute('previousExpression'); + + // [PhpParser\Node\Expr\Assign] $variable = new Class() + if ($previousExpression instanceof Expression) { + $class = $this->resolveFromExpression($previousExpression); + } + + if ($previousExpression instanceof ClassMethod) { + $class = $this->resolveFromClassMethod($previousExpression, $methodCall); + } + + return $class; + } + + private function resolveFromExpression(Expression $expression): ?FullyQualified + { + $assign = $expression->expr; + if (! $assign instanceof Assign) { + return null; + } + + $new = $assign->expr; + if (! $new instanceof New_) { + return null; + } + + $class = $new->class; + + return $class instanceof FullyQualified ? $class : null; + } + + private function resolveFromClassMethod(ClassMethod $classMethod, MethodCall $methodCall): ?FullyQualified + { + $class = $this->tryToResolveClassMethodStmts($classMethod); + + if ($class === null) { + $class = $this->tryToResolveClassMethodParams($classMethod, $methodCall); + } + + return $class; + } + + private function tryToResolveClassMethodStmts(ClassMethod $classMethod): ?FullyQualified + { + // $ this -> method(); + $stmts = $classMethod->stmts; + if ($stmts === null) { + return null; + } + + /** @var Stmt $stmt */ + foreach ($stmts as $stmt) { + if ($stmt->expr->var->name === 'this') { + $class = $classMethod->name->getAttribute(ClassLike::class)->extends; + return $class instanceof FullyQualified ? $class : null; + } + } + + return null; + } + + private function tryToResolveClassMethodParams(ClassMethod $classMethod, MethodCall $methodCall): ?FullyQualified + { + // $ param -> method(); + $params = $classMethod->params; + /** @var Param $param */ + foreach ($params as $param) { + $paramVar = $param->var; + $methodCallVar = $methodCall->var; + if (! $paramVar instanceof Variable || ! $methodCallVar instanceof Variable) { + continue; + } + if ($paramVar->name === $methodCallVar->name) { + $class = $param->type; + return $class instanceof FullyQualified ? $class : null; + } + } + + return null; + } +} diff --git a/src/PhpParser/Parser/FunctionParser.php b/src/PhpParser/Parser/FunctionParser.php new file mode 100644 index 000000000000..5dd00618494e --- /dev/null +++ b/src/PhpParser/Parser/FunctionParser.php @@ -0,0 +1,44 @@ +parser = $parser; + } + + public function parseFunction(ReflectionFunction $reflectionFunction): ?Namespace_ + { + $fileName = $reflectionFunction->getFileName(); + if (! is_string($fileName)) { + return null; + } + + $functionCode = FileSystem::read($fileName); + if (! is_string($functionCode)) { + return null; + } + + $ast = $this->parser->parse($functionCode)[0]; + + if (! $ast instanceof Namespace_) { + return null; + } + + return $ast; + } +} diff --git a/src/Reflection/ClassMethodReflectionFactory.php b/src/Reflection/ClassMethodReflectionFactory.php index 91de0e12f9f8..bb1208752dad 100644 --- a/src/Reflection/ClassMethodReflectionFactory.php +++ b/src/Reflection/ClassMethodReflectionFactory.php @@ -41,7 +41,7 @@ public function createFromPHPStanTypeAndMethodName(Type $type, string $methodNam return null; } - private function createReflectionMethodIfExists(string $class, string $method): ?ReflectionMethod + public function createReflectionMethodIfExists(string $class, string $method): ?ReflectionMethod { if (! method_exists($class, $method)) { return null; diff --git a/src/Reflection/ClassMethodReflectionHelper.php b/src/Reflection/ClassMethodReflectionHelper.php new file mode 100644 index 000000000000..dd12705ce133 --- /dev/null +++ b/src/Reflection/ClassMethodReflectionHelper.php @@ -0,0 +1,53 @@ +classMethodReflectionFactory = $classMethodReflectionFactory; + $this->phpDocTagsFinder = $phpDocTagsFinder; + } + + public function extractTagsFromMethodDockblock(string $class, string $method, string $tag): array + { + $reflectedMethod = $this->classMethodReflectionFactory->createReflectionMethodIfExists($class, $method); + + if ($reflectedMethod === null) { + return []; + } + + $methodDocblock = $reflectedMethod->getDocComment(); + + if (! is_string($methodDocblock)) { + return []; + } + + $returnTags = $this->phpDocTagsFinder->extractTagsFromStringedDocblock($methodDocblock, $tag); + + $returnClasses = []; + foreach ($returnTags as $returnTag) { + $returnClasses[] = Reflection::expandClassName($returnTag, $reflectedMethod->getDeclaringClass()); + } + + return $returnClasses; + } +} diff --git a/src/Reflection/FunctionReflectionHelper.php b/src/Reflection/FunctionReflectionHelper.php new file mode 100644 index 000000000000..e3a663f8042f --- /dev/null +++ b/src/Reflection/FunctionReflectionHelper.php @@ -0,0 +1,95 @@ +functionParser = $functionParser; + $this->classNaming = $classNaming; + $this->phpDocTagsFinder = $phpDocTagsFinder; + } + + public function extractFunctionAnnotatedThrows(ReflectionFunction $reflectionFunction): array + { + $functionDocblock = $reflectionFunction->getDocComment(); + + if (! is_string($functionDocblock)) { + return []; + } + + $annotatedThrownClasses = $this->phpDocTagsFinder->extractTagsFromStringedDocblock( + $functionDocblock, + '@throws' + ); + + return $this->expandAnnotatedClasses($reflectionFunction, $annotatedThrownClasses); + } + + public function expandAnnotatedClasses(ReflectionFunction $reflectionFunction, array $classNames): array + { + $functionNode = $this->functionParser->parseFunction($reflectionFunction); + if (! $functionNode instanceof Namespace_) { + return []; + } + + $uses = $this->getUses($functionNode); + + $expandedClasses = []; + foreach ($classNames as $className) { + $shortClassName = $this->classNaming->getShortName($className); + $expandedClasses[] = $uses[$shortClassName] ?? $className; + } + + return $expandedClasses; + } + + private function getUses(Namespace_ $node): array + { + $uses = []; + foreach ($node->stmts as $stmt) { + if (! $stmt instanceof Use_) { + continue; + } + + $use = $stmt->uses[0]; + if (! $use instanceof UseUse) { + continue; + } + + $parts = $use->name->parts; + $uses[$parts[count($parts) - 1]] = implode('\\', $parts); + } + + return $uses; + } +}