Skip to content

Commit

Permalink
add FQN support resolver to DoctrineAnnotationClassToAttributeRector (#…
Browse files Browse the repository at this point in the history
…1261)

Co-authored-by: GitHub Action <action@github.com>
  • Loading branch information
TomasVotruba and actions-user committed Nov 17, 2021
1 parent d2f7c2e commit b201787
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ public function findDoctrineAnnotationsByClass(PhpDocNode $phpDocNode, string $d

/** @var DoctrineAnnotationTagValueNode[] $doctrineTagValueNodes */
$doctrineTagValueNodes = $this->findByType($phpDocNode, DoctrineAnnotationTagValueNode::class);

foreach ($doctrineTagValueNodes as $doctrineTagValueNode) {
if ($doctrineTagValueNode->hasClassName($desiredClass)) {
$desiredDoctrineTagValueNodes[] = $doctrineTagValueNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public function resolveTagFullyQualifiedName(string $tag, Node $node): string
private function resolveFullyQualifiedClass(array $uses, Node $node, string $tag): string
{
$scope = $node->getAttribute(AttributeKey::SCOPE);

if ($scope instanceof Scope) {
$namespace = $scope->getNamespace();
if ($namespace !== null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

namespace Rector\BetterPhpDocParser\PhpDocParser;

use Nette\Utils\Strings;
use PhpParser\Node;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use Rector\BetterPhpDocParser\Attributes\AttributeMirrorer;
Expand All @@ -28,6 +30,12 @@ final class DoctrineAnnotationDecorator
*/
private const ALLOWED_SHORT_ANNOTATIONS = ['Target'];

/**
* @see https://regex101.com/r/95kIw4/1
* @var string
*/
private const LONG_ANNOTATION_REGEX = '#@\\\\(?<class_name>.*?)(?<annotation_content>\(.*?\))#';

public function __construct(
private CurrentNodeProvider $currentNodeProvider,
private ClassAnnotationMatcher $classAnnotationMatcher,
Expand Down Expand Up @@ -133,6 +141,31 @@ private function transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes(
Node $currentPhpNode
): void {
foreach ($phpDocNode->children as $key => $phpDocChildNode) {
// the @\FQN use case
if ($phpDocChildNode instanceof PhpDocTextNode) {
$match = Strings::match($phpDocChildNode->text, self::LONG_ANNOTATION_REGEX);
$fullyQualifiedAnnotationClass = $match['class_name'] ?? null;

if ($fullyQualifiedAnnotationClass === null) {
continue;
}

$annotationContent = $match['annotation_content'] ?? null;
$tagName = '@\\' . $fullyQualifiedAnnotationClass;

$formerStartEnd = $phpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END);

$spacelessPhpDocTagNode = $this->createDoctrineSpacelessPhpDocTagNode(
$annotationContent,
$tagName,
$fullyQualifiedAnnotationClass,
$formerStartEnd
);

$phpDocNode->children[$key] = $spacelessPhpDocTagNode;
continue;
}

if (! $phpDocChildNode instanceof PhpDocTagNode) {
continue;
}
Expand All @@ -156,30 +189,10 @@ private function transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes(
continue;
}

$genericTagValueNode = $phpDocChildNode->value;
$nestedTokenIterator = $this->tokenIteratorFactory->create($genericTagValueNode->value);

// mimics doctrine behavior just in phpdoc-parser syntax :)
// https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L742
$values = $this->staticDoctrineAnnotationParser->resolveAnnotationMethodCall($nestedTokenIterator);

$formerStartEnd = $genericTagValueNode->getAttribute(PhpDocAttributeKey::START_AND_END);

$identifierTypeNode = new IdentifierTypeNode($phpDocChildNode->name);
$identifierTypeNode->setAttribute(PhpDocAttributeKey::RESOLVED_CLASS, $fullyQualifiedAnnotationClass);

$doctrineAnnotationTagValueNode = new DoctrineAnnotationTagValueNode(
$identifierTypeNode,
$genericTagValueNode->value,
$values,
SilentKeyMap::CLASS_NAMES_TO_SILENT_KEYS[$fullyQualifiedAnnotationClass] ?? null
);

$doctrineAnnotationTagValueNode->setAttribute(PhpDocAttributeKey::START_AND_END, $formerStartEnd);

$spacelessPhpDocTagNode = new SpacelessPhpDocTagNode(
$spacelessPhpDocTagNode = $this->createSpacelessPhpDocTagNode(
$phpDocChildNode->name,
$doctrineAnnotationTagValueNode
$phpDocChildNode->value,
$fullyQualifiedAnnotationClass
);

$this->attributeMirrorer->mirror($phpDocChildNode, $spacelessPhpDocTagNode);
Expand Down Expand Up @@ -223,4 +236,48 @@ private function isClosedContent(string $composedContent): bool

return $openBracketCount === $closeBracketCount;
}

private function createSpacelessPhpDocTagNode(
string $tagName,
GenericTagValueNode $genericTagValueNode,
string $fullyQualifiedAnnotationClass
): SpacelessPhpDocTagNode {
$annotationContent = $genericTagValueNode->value;
$formerStartEnd = $genericTagValueNode->getAttribute(PhpDocAttributeKey::START_AND_END);

return $this->createDoctrineSpacelessPhpDocTagNode(
$annotationContent,
$tagName,
$fullyQualifiedAnnotationClass,
$formerStartEnd
);
}

private function createDoctrineSpacelessPhpDocTagNode(
string $annotationContent,
string $tagName,
string $fullyQualifiedAnnotationClass,
StartAndEnd $startAndEnd
): SpacelessPhpDocTagNode {
$nestedTokenIterator = $this->tokenIteratorFactory->create($annotationContent);

// mimics doctrine behavior just in phpdoc-parser syntax :)
// https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L742

$values = $this->staticDoctrineAnnotationParser->resolveAnnotationMethodCall($nestedTokenIterator);

$identifierTypeNode = new IdentifierTypeNode($tagName);
$identifierTypeNode->setAttribute(PhpDocAttributeKey::RESOLVED_CLASS, $fullyQualifiedAnnotationClass);

$doctrineAnnotationTagValueNode = new DoctrineAnnotationTagValueNode(
$identifierTypeNode,
$annotationContent,
$values,
SilentKeyMap::CLASS_NAMES_TO_SILENT_KEYS[$fullyQualifiedAnnotationClass] ?? null
);

$doctrineAnnotationTagValueNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd);

return new SpacelessPhpDocTagNode($tagName, $doctrineAnnotationTagValueNode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Rector\Tests\Php80\Rector\Class_\DoctrineAnnotationClassToAttributeRector\Fixture;

/**
* @Annotation
* @\Doctrine\Common\Annotations\Annotation\Target("METHOD")
*/
final class FullyQualifiedTarget
{
}

?>
-----
<?php

namespace Rector\Tests\Php80\Rector\Class_\DoctrineAnnotationClassToAttributeRector\Fixture;

#[\Attribute(\Attribute::TARGET_METHOD)]
final class FullyQualifiedTarget
{
}

?>
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ private function decorateTarget(PhpDocInfo $phpDocInfo, AttributeGroup $attribut
}

$targets = $targetDoctrineAnnotationTagValueNode->getSilentValue();

if ($targets instanceof CurlyListNode) {
$targetValues = $targets->getValuesWithExplicitSilentAndWithoutQuotes();
} elseif (is_string($targets)) {
Expand Down

0 comments on commit b201787

Please sign in to comment.