Skip to content
Closed
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
],
"files": [
"packages/better-php-doc-parser/tests/PhpDocInfo/PhpDocInfoPrinter/AbstractPhpDocInfoPrinterTest.php",
Comment thread
Aerendir marked this conversation as resolved.
Outdated
"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",
Expand Down
117 changes: 110 additions & 7 deletions rules/coding-style/src/Rector/Throw_/AnnotateThrowablesRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@

use Nette\Utils\Reflection;
use Nette\Utils\Strings;
use PhpParser\Builder\Function_;
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\Name\FullyQualified;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Throw_;
use PhpParser\ParserFactory;
use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\Type\ObjectType;
Expand All @@ -23,6 +27,7 @@
use Rector\Core\RectorDefinition\RectorDefinition;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PHPStan\Type\ShortenedObjectType;
use ReflectionFunction;
use ReflectionMethod;

/**
Expand All @@ -35,6 +40,8 @@ final class AnnotateThrowablesRector extends AbstractRector
*/
private const RETURN_DOCBLOCK_TAG_REGEX = '#@return[ a-zA-Z0-9\|\\\t]+#';

private const THROWS_DOCBLOCK_TAG_REGEX = '#@throws[ a-zA-Z0-9\|\\\t]+#';

/**
* @var array
*/
Expand All @@ -45,7 +52,7 @@ final class AnnotateThrowablesRector extends AbstractRector
*/
public function getNodeTypes(): array
{
return [Throw_::class];
return [Throw_::class, FuncCall::class];
}

/**
Expand Down Expand Up @@ -93,20 +100,45 @@ public function throwException(int $code)
);
}

/**
* @param Throw_ $node
*/

public function refactor(Node $node): ?Node
{
if ($this->isThrowableAnnotated($node)) {
return null;
if ($node instanceof Throw_) {
if ($this->isThrowableAnnotated($node)) {
return null;
}

$this->annotateThrowable($node);
}

$this->annotateThrowable($node);
if ($node instanceof FuncCall) {
if ($this->hasFunctionAnnotatedThrowables($node) === false) {
return null;
}

$this->annotateThrowablesFromFunctionCall($node);
}

if ($node instanceof Function_ || $node instanceof ClassMethod) {
foreach ($node->stmts as $stmt) {
if ($stmt instanceof Throw_) {
return null;
}
}
}

return $node;
}

private function hasFunctionAnnotatedThrowables(FuncCall $funcCall): bool
{
$functionFqn = implode('\\', $funcCall->name->getAttribute('namespacedName')->parts);
$functionFqn = strtolower($functionFqn);
$throws = $this->extractFunctionThrowsFromDockblock($functionFqn);

return empty($throws) === false;
}

private function isThrowableAnnotated(Throw_ $throw): bool
{
$phpDocInfo = $this->getThrowingStmtPhpDocInfo($throw);
Expand Down Expand Up @@ -202,6 +234,55 @@ private function extractMethodReturnsFromDocblock(string $classFqn, string $meth
return $returnClasses;
}

private function extractFunctionThrowsFromDockblock(string $functionFqn): array
{
$reflectedFunction = new ReflectionFunction($functionFqn);
$functionDocblock = $reflectedFunction->getDocComment();

// copied from https://github.com/nette/di/blob/d1c0598fdecef6d3b01e2ace5f2c30214b3108e6/src/DI/Autowiring.php#L215
$result = Strings::matchAll((string) $functionDocblock, self::THROWS_DOCBLOCK_TAG_REGEX);
if ($result === null) {
return [];
}

$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse(file_get_contents($reflectedFunction->getFileName()))[0];

if (! $ast instanceof Node\Stmt\Namespace_) {
return [];
}

$uses = [];
foreach ($ast->stmts as $stmt) {
if (! $stmt instanceof Node\Stmt\Use_) {
continue;
}

$use = $stmt->uses[0];
if (! $use instanceof Node\Stmt\UseUse) {
continue;
}

$parts = $use->name->parts;
$uses[$parts[count($parts) - 1]] = implode('\\', $parts);
}

$throwsClasses = [];
foreach ($result as $throwsTag) {
$throwsTag = str_replace('@throws ', '', $throwsTag[0]);
$throwsTagParts = explode('\\', $throwsTag);
$throwsTagShortName = $throwsTagParts[count($throwsTagParts) - 1];

if (key_exists($throwsTagShortName, $uses)) {
$throwsClasses[] = $uses[$throwsTagShortName];
}
}

$this->foundThrownClasses = $throwsClasses;

return $throwsClasses;
}

private function annotateThrowable(Throw_ $node): void
{
$throwClass = $this->buildFQN($node);
Expand All @@ -223,6 +304,28 @@ private function annotateThrowable(Throw_ $node): void
$this->foundThrownClasses = [];
}

private function annotateThrowablesFromFunctionCall(FuncCall $funcCall): void
{
if (empty($this->foundThrownClasses)) {
return;
}

$callee = $funcCall->getAttribute('previousExpression');
while (true) {
if ($callee instanceof ClassMethod) {
break;
}
}

$calleePhpDocInfo = $callee->getAttribute(AttributeKey::PHP_DOC_INFO);
foreach ($this->foundThrownClasses as $thrownClass) {
$docComment = $this->buildThrowsDocComment($thrownClass);
$calleePhpDocInfo->addPhpDocTagNode($docComment);
}

$this->foundThrownClasses = [];
}

private function buildThrowsDocComment(string $throwableClass): AttributeAwarePhpDocTagNode
{
$genericTagValueNode = new ThrowsTagValueNode(new IdentifierTypeNode('\\' . $throwableClass), '');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Rector\CodingStyle\Tests\Rector\Throw_\AnnotateThrowablesRector\Fixture;

class UseOfAFunctionThatThrowsAnException
{
public function iUseAFunctionThatMayThrowAnException()
{
return i_throw_an_exception();
}
}

?>
-----
<?php

namespace Rector\CodingStyle\Tests\Rector\Throw_\AnnotateThrowablesRector\Fixture;

class UseOfAFunctionThatThrowsAnException
{
/**
* @throws \Rector\CodingStyle\Tests\Rector\Throw_\AnnotateThrowablesRector\Source\Exceptions\TheException
* @throws \Rector\CodingStyle\Tests\Rector\Throw_\AnnotateThrowablesRector\Source\Exceptions\TheExceptionTheSecond
*/
public function iUseAFunctionThatMayThrowAnException()
{
return i_throw_an_exception();
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Rector\CodingStyle\Tests\Rector\Throw_\AnnotateThrowablesRector\Fixture;

use Rector\CodingStyle\Tests\Rector\Throw_\AnnotateThrowablesRector\Source\Exceptions\TheException;
use Rector\CodingStyle\Tests\Rector\Throw_\AnnotateThrowablesRector\Source\Exceptions\TheExceptionTheSecond;


/**
* @param null|string $switch
* @throws TheException
* @throws TheExceptionTheSecond
*/
function i_throw_an_exception(?string $switch = null):bool
{
if (null === $switch) {
throw new TheExceptionTheSecond("I'm a function that throws an exception.");
}

throw new TheException("I'm a function that throws an exception.");
}