2021-04-04 09:01:11 +00:00
|
|
|
<?php
|
|
|
|
|
2021-05-09 20:15:43 +00:00
|
|
|
declare (strict_types=1);
|
2022-06-06 17:12:56 +00:00
|
|
|
namespace Rector\BetterPhpDocParser\PhpDocParser;
|
2021-04-04 09:01:11 +00:00
|
|
|
|
2023-01-01 00:36:31 +00:00
|
|
|
use RectorPrefix202301\Nette\Utils\Strings;
|
2022-06-06 17:12:56 +00:00
|
|
|
use PhpParser\Node;
|
|
|
|
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
|
|
|
|
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
|
|
|
|
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;
|
|
|
|
use Rector\BetterPhpDocParser\Contract\PhpDocParser\PhpDocNodeDecoratorInterface;
|
|
|
|
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
|
|
|
|
use Rector\BetterPhpDocParser\PhpDoc\SpacelessPhpDocTagNode;
|
|
|
|
use Rector\BetterPhpDocParser\PhpDocInfo\TokenIteratorFactory;
|
|
|
|
use Rector\BetterPhpDocParser\ValueObject\DoctrineAnnotation\SilentKeyMap;
|
|
|
|
use Rector\BetterPhpDocParser\ValueObject\PhpDocAttributeKey;
|
|
|
|
use Rector\BetterPhpDocParser\ValueObject\StartAndEnd;
|
|
|
|
use Rector\Core\Util\StringUtils;
|
2022-06-07 08:22:29 +00:00
|
|
|
final class DoctrineAnnotationDecorator implements PhpDocNodeDecoratorInterface
|
2021-04-04 09:01:11 +00:00
|
|
|
{
|
2021-11-17 17:46:14 +00:00
|
|
|
/**
|
|
|
|
* Special short annotations, that are resolved as FQN by Doctrine annotation parser
|
|
|
|
* @var string[]
|
|
|
|
*/
|
|
|
|
private const ALLOWED_SHORT_ANNOTATIONS = ['Target'];
|
2021-11-17 21:39:26 +00:00
|
|
|
/**
|
|
|
|
* @see https://regex101.com/r/95kIw4/1
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private const LONG_ANNOTATION_REGEX = '#@\\\\(?<class_name>.*?)(?<annotation_content>\\(.*?\\))#';
|
2021-11-20 10:45:53 +00:00
|
|
|
/**
|
|
|
|
* @see https://regex101.com/r/xWaLOz/1
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private const NESTED_ANNOTATION_END_REGEX = '#(\\s+)?\\}\\)(\\s+)?#';
|
2021-04-04 09:01:11 +00:00
|
|
|
/**
|
2021-12-04 12:47:17 +00:00
|
|
|
* @readonly
|
2021-05-10 23:39:21 +00:00
|
|
|
* @var \Rector\BetterPhpDocParser\PhpDocParser\ClassAnnotationMatcher
|
2021-04-04 09:01:11 +00:00
|
|
|
*/
|
|
|
|
private $classAnnotationMatcher;
|
|
|
|
/**
|
2021-12-04 12:47:17 +00:00
|
|
|
* @readonly
|
2021-05-10 23:39:21 +00:00
|
|
|
* @var \Rector\BetterPhpDocParser\PhpDocParser\StaticDoctrineAnnotationParser
|
2021-04-04 09:01:11 +00:00
|
|
|
*/
|
|
|
|
private $staticDoctrineAnnotationParser;
|
|
|
|
/**
|
2021-12-04 12:47:17 +00:00
|
|
|
* @readonly
|
2021-05-10 23:39:21 +00:00
|
|
|
* @var \Rector\BetterPhpDocParser\PhpDocInfo\TokenIteratorFactory
|
2021-04-04 09:01:11 +00:00
|
|
|
*/
|
|
|
|
private $tokenIteratorFactory;
|
2021-04-06 18:36:50 +00:00
|
|
|
/**
|
2021-12-04 12:47:17 +00:00
|
|
|
* @readonly
|
2021-05-10 23:39:21 +00:00
|
|
|
* @var \Rector\BetterPhpDocParser\Attributes\AttributeMirrorer
|
2021-04-06 18:36:50 +00:00
|
|
|
*/
|
|
|
|
private $attributeMirrorer;
|
2022-06-07 08:22:29 +00:00
|
|
|
public function __construct(\Rector\BetterPhpDocParser\PhpDocParser\ClassAnnotationMatcher $classAnnotationMatcher, \Rector\BetterPhpDocParser\PhpDocParser\StaticDoctrineAnnotationParser $staticDoctrineAnnotationParser, TokenIteratorFactory $tokenIteratorFactory, AttributeMirrorer $attributeMirrorer)
|
2021-05-09 20:15:43 +00:00
|
|
|
{
|
2021-04-04 09:01:11 +00:00
|
|
|
$this->classAnnotationMatcher = $classAnnotationMatcher;
|
|
|
|
$this->staticDoctrineAnnotationParser = $staticDoctrineAnnotationParser;
|
|
|
|
$this->tokenIteratorFactory = $tokenIteratorFactory;
|
2021-04-06 18:36:50 +00:00
|
|
|
$this->attributeMirrorer = $attributeMirrorer;
|
2021-04-04 09:01:11 +00:00
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
public function decorate(PhpDocNode $phpDocNode, Node $phpNode) : void
|
2021-04-04 09:01:11 +00:00
|
|
|
{
|
|
|
|
// merge split doctrine nested tags
|
|
|
|
$this->mergeNestedDoctrineAnnotations($phpDocNode);
|
2022-06-02 09:05:21 +00:00
|
|
|
$this->transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes($phpDocNode, $phpNode);
|
2021-04-04 09:01:11 +00:00
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Join token iterator with all the following nodes if nested
|
|
|
|
*/
|
2022-06-07 08:22:29 +00:00
|
|
|
private function mergeNestedDoctrineAnnotations(PhpDocNode $phpDocNode) : void
|
2021-04-04 09:01:11 +00:00
|
|
|
{
|
|
|
|
$removedKeys = [];
|
|
|
|
foreach ($phpDocNode->children as $key => $phpDocChildNode) {
|
2021-05-09 20:15:43 +00:00
|
|
|
if (\in_array($key, $removedKeys, \true)) {
|
2021-04-04 09:01:11 +00:00
|
|
|
continue;
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
if (!$phpDocChildNode instanceof PhpDocTagNode) {
|
2021-04-04 09:01:11 +00:00
|
|
|
continue;
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
if (!$phpDocChildNode->value instanceof GenericTagValueNode) {
|
2021-04-04 09:01:11 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$genericTagValueNode = $phpDocChildNode->value;
|
2021-04-13 18:51:41 +00:00
|
|
|
while (isset($phpDocNode->children[$key])) {
|
|
|
|
++$key;
|
|
|
|
// no more next nodes
|
2021-05-09 20:15:43 +00:00
|
|
|
if (!isset($phpDocNode->children[$key])) {
|
2021-04-13 18:51:41 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
$nextPhpDocChildNode = $phpDocNode->children[$key];
|
2022-06-07 08:22:29 +00:00
|
|
|
if ($nextPhpDocChildNode instanceof PhpDocTextNode && StringUtils::isMatch($nextPhpDocChildNode->text, self::NESTED_ANNOTATION_END_REGEX)) {
|
2021-11-20 10:45:53 +00:00
|
|
|
// @todo how to detect previously opened brackets?
|
|
|
|
// probably local property with holding count of opened brackets
|
|
|
|
$composedContent = $genericTagValueNode->value . \PHP_EOL . $nextPhpDocChildNode->text;
|
|
|
|
$genericTagValueNode->value = $composedContent;
|
|
|
|
$startAndEnd = $this->combineStartAndEnd($phpDocChildNode, $nextPhpDocChildNode);
|
2022-06-07 08:22:29 +00:00
|
|
|
$phpDocChildNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd);
|
2021-11-20 10:45:53 +00:00
|
|
|
$removedKeys[] = $key;
|
|
|
|
$removedKeys[] = $key + 1;
|
|
|
|
continue;
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
if (!$nextPhpDocChildNode instanceof PhpDocTagNode) {
|
2021-04-04 09:01:11 +00:00
|
|
|
continue;
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
if (!$nextPhpDocChildNode->value instanceof GenericTagValueNode) {
|
2021-04-04 09:01:11 +00:00
|
|
|
continue;
|
|
|
|
}
|
2021-04-13 18:51:41 +00:00
|
|
|
if ($this->isClosedContent($genericTagValueNode->value)) {
|
|
|
|
break;
|
|
|
|
}
|
2021-11-20 10:45:53 +00:00
|
|
|
$composedContent = $genericTagValueNode->value . \PHP_EOL . $nextPhpDocChildNode->name . $nextPhpDocChildNode->value->value;
|
|
|
|
// cleanup the next from closing
|
2021-04-13 18:51:41 +00:00
|
|
|
$genericTagValueNode->value = $composedContent;
|
2021-11-20 10:45:53 +00:00
|
|
|
$startAndEnd = $this->combineStartAndEnd($phpDocChildNode, $nextPhpDocChildNode);
|
2022-06-07 08:22:29 +00:00
|
|
|
$phpDocChildNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd);
|
2021-04-04 09:01:11 +00:00
|
|
|
$currentChildValueNode = $phpDocNode->children[$key];
|
2022-06-07 08:22:29 +00:00
|
|
|
if (!$currentChildValueNode instanceof PhpDocTagNode) {
|
2021-04-04 09:01:11 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$currentGenericTagValueNode = $currentChildValueNode->value;
|
2022-06-07 08:22:29 +00:00
|
|
|
if (!$currentGenericTagValueNode instanceof GenericTagValueNode) {
|
2021-04-04 09:01:11 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$removedKeys[] = $key;
|
|
|
|
}
|
|
|
|
}
|
2021-05-09 20:15:43 +00:00
|
|
|
foreach (\array_keys($phpDocNode->children) as $key) {
|
|
|
|
if (!\in_array($key, $removedKeys, \true)) {
|
2021-04-04 09:01:11 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
unset($phpDocNode->children[$key]);
|
|
|
|
}
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
private function transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes(PhpDocNode $phpDocNode, Node $currentPhpNode) : void
|
2021-05-09 20:15:43 +00:00
|
|
|
{
|
2021-04-13 18:51:41 +00:00
|
|
|
foreach ($phpDocNode->children as $key => $phpDocChildNode) {
|
2021-11-17 21:39:26 +00:00
|
|
|
// the @\FQN use case
|
2022-06-07 08:22:29 +00:00
|
|
|
if ($phpDocChildNode instanceof PhpDocTextNode) {
|
2021-11-20 10:45:53 +00:00
|
|
|
$spacelessPhpDocTagNode = $this->resolveFqnAnnotationSpacelessPhpDocTagNode($phpDocChildNode);
|
2022-06-07 08:22:29 +00:00
|
|
|
if (!$spacelessPhpDocTagNode instanceof SpacelessPhpDocTagNode) {
|
2021-11-17 21:39:26 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$phpDocNode->children[$key] = $spacelessPhpDocTagNode;
|
|
|
|
continue;
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
if (!$phpDocChildNode instanceof PhpDocTagNode) {
|
2021-04-13 18:51:41 +00:00
|
|
|
continue;
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
if (!$phpDocChildNode->value instanceof GenericTagValueNode) {
|
2021-04-13 18:51:41 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// known doc tag to annotation class
|
2022-06-06 08:39:49 +00:00
|
|
|
$fullyQualifiedAnnotationClass = (string) $this->classAnnotationMatcher->resolveTagFullyQualifiedName($phpDocChildNode->name, $currentPhpNode);
|
2021-04-13 18:51:41 +00:00
|
|
|
// not an annotations class
|
2021-11-17 17:46:14 +00:00
|
|
|
if (\strpos($fullyQualifiedAnnotationClass, '\\') === \false && !\in_array($fullyQualifiedAnnotationClass, self::ALLOWED_SHORT_ANNOTATIONS, \true)) {
|
2021-04-13 18:51:41 +00:00
|
|
|
continue;
|
|
|
|
}
|
2021-11-17 21:39:26 +00:00
|
|
|
$spacelessPhpDocTagNode = $this->createSpacelessPhpDocTagNode($phpDocChildNode->name, $phpDocChildNode->value, $fullyQualifiedAnnotationClass);
|
2021-04-13 18:51:41 +00:00
|
|
|
$this->attributeMirrorer->mirror($phpDocChildNode, $spacelessPhpDocTagNode);
|
|
|
|
$phpDocNode->children[$key] = $spacelessPhpDocTagNode;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* This is closed block, e.g. {( ... )},
|
|
|
|
* false on: {( ... )
|
|
|
|
*/
|
2021-05-09 20:15:43 +00:00
|
|
|
private function isClosedContent(string $composedContent) : bool
|
2021-04-13 18:51:41 +00:00
|
|
|
{
|
|
|
|
$composedTokenIterator = $this->tokenIteratorFactory->create($composedContent);
|
|
|
|
$tokenCount = $composedTokenIterator->count();
|
|
|
|
$openBracketCount = 0;
|
|
|
|
$closeBracketCount = 0;
|
2021-04-13 18:58:34 +00:00
|
|
|
if ($composedContent === '') {
|
2021-05-09 20:15:43 +00:00
|
|
|
return \true;
|
2021-04-13 18:58:34 +00:00
|
|
|
}
|
2021-04-13 18:51:41 +00:00
|
|
|
do {
|
2022-06-09 12:53:21 +00:00
|
|
|
if ($composedTokenIterator->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET, Lexer::TOKEN_OPEN_PARENTHESES) || \strpos($composedTokenIterator->currentTokenValue(), '(') !== \false) {
|
2021-04-13 18:51:41 +00:00
|
|
|
++$openBracketCount;
|
|
|
|
}
|
2022-06-09 12:53:21 +00:00
|
|
|
if ($composedTokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET, Lexer::TOKEN_CLOSE_PARENTHESES) || \strpos($composedTokenIterator->currentTokenValue(), ')') !== \false) {
|
2021-04-13 18:51:41 +00:00
|
|
|
++$closeBracketCount;
|
|
|
|
}
|
|
|
|
$composedTokenIterator->next();
|
2021-05-09 20:15:43 +00:00
|
|
|
} while ($composedTokenIterator->currentPosition() < $tokenCount - 1);
|
2021-04-13 18:51:41 +00:00
|
|
|
return $openBracketCount === $closeBracketCount;
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
private function createSpacelessPhpDocTagNode(string $tagName, GenericTagValueNode $genericTagValueNode, string $fullyQualifiedAnnotationClass) : SpacelessPhpDocTagNode
|
2021-11-17 21:39:26 +00:00
|
|
|
{
|
2022-06-07 08:22:29 +00:00
|
|
|
$formerStartEnd = $genericTagValueNode->getAttribute(PhpDocAttributeKey::START_AND_END);
|
2022-06-04 18:08:03 +00:00
|
|
|
return $this->createDoctrineSpacelessPhpDocTagNode($genericTagValueNode->value, $tagName, $fullyQualifiedAnnotationClass, $formerStartEnd);
|
2021-11-17 21:39:26 +00:00
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
private function createDoctrineSpacelessPhpDocTagNode(string $annotationContent, string $tagName, string $fullyQualifiedAnnotationClass, StartAndEnd $startAndEnd) : SpacelessPhpDocTagNode
|
2021-11-17 21:39:26 +00:00
|
|
|
{
|
|
|
|
$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);
|
2022-06-07 08:22:29 +00:00
|
|
|
$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);
|
2021-11-17 21:39:26 +00:00
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
private function combineStartAndEnd(\PHPStan\PhpDocParser\Ast\Node $startPhpDocChildNode, PhpDocChildNode $endPhpDocChildNode) : StartAndEnd
|
2021-11-20 10:45:53 +00:00
|
|
|
{
|
|
|
|
/** @var StartAndEnd $currentStartAndEnd */
|
2022-06-07 08:22:29 +00:00
|
|
|
$currentStartAndEnd = $startPhpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END);
|
2021-11-20 10:45:53 +00:00
|
|
|
/** @var StartAndEnd $nextStartAndEnd */
|
2022-06-07 08:22:29 +00:00
|
|
|
$nextStartAndEnd = $endPhpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END);
|
|
|
|
return new StartAndEnd($currentStartAndEnd->getStart(), $nextStartAndEnd->getEnd());
|
2021-11-20 10:45:53 +00:00
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
private function resolveFqnAnnotationSpacelessPhpDocTagNode(PhpDocTextNode $phpDocTextNode) : ?SpacelessPhpDocTagNode
|
2021-11-20 10:45:53 +00:00
|
|
|
{
|
2022-06-07 08:22:29 +00:00
|
|
|
$match = Strings::match($phpDocTextNode->text, self::LONG_ANNOTATION_REGEX);
|
2021-11-20 10:45:53 +00:00
|
|
|
$fullyQualifiedAnnotationClass = $match['class_name'] ?? null;
|
|
|
|
if ($fullyQualifiedAnnotationClass === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
$annotationContent = $match['annotation_content'] ?? null;
|
|
|
|
$tagName = '@\\' . $fullyQualifiedAnnotationClass;
|
2022-06-07 08:22:29 +00:00
|
|
|
$formerStartEnd = $phpDocTextNode->getAttribute(PhpDocAttributeKey::START_AND_END);
|
2021-11-20 10:45:53 +00:00
|
|
|
return $this->createDoctrineSpacelessPhpDocTagNode($annotationContent, $tagName, $fullyQualifiedAnnotationClass, $formerStartEnd);
|
|
|
|
}
|
2021-04-04 09:01:11 +00:00
|
|
|
}
|