Skip to content
Merged
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",
"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
254 changes: 154 additions & 100 deletions rules/coding-style/src/Rector/Throw_/AnnotateThrowablesRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,78 @@

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
*/
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];
}

/**
Expand Down Expand Up @@ -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);
Comment thread
Aerendir marked this conversation as resolved.

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]);
}
}
Loading