[PHP 8.0] Add support for nested annotation to attributes (#342)

This commit is contained in:
Tomas Votruba 2021-06-30 23:39:28 +02:00 committed by GitHub
parent 4c4049217a
commit 8733e7335f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 265 additions and 110 deletions

View File

@ -87,7 +87,7 @@ use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
/** @var PhpDocInfo $phpDocInfo */
$entityTagValueNode = $phpDocInfo->getByAnnotationClass('Doctrine\ORM\Mapping\Entity');
$entityTagValueNode = $phpDocInfo->findOneByAnnotationClass('Doctrine\ORM\Mapping\Entity');
if (! $entityTagValueNode instanceof DoctrineAnnotationTagValueNode) {
return null;
}

View File

@ -55,7 +55,7 @@ final class TestModifyReprintTest extends AbstractTestCase
$phpDocInfo = $this->parseFileAndGetFirstNodeOfType($inputFileInfo, ClassMethod::class);
/** @var DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode */
$doctrineAnnotationTagValueNode = $phpDocInfo->getByAnnotationClass(
$doctrineAnnotationTagValueNode = $phpDocInfo->findOneByAnnotationClass(
'Symfony\Component\Routing\Annotation\Route'
);
// this will extended tokens of first node

View File

@ -26,6 +26,7 @@ use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\BetterPhpDocParser\PhpDoc\SpacelessPhpDocTagNode;
use Rector\BetterPhpDocParser\PhpDocNodeVisitor\ChangedPhpDocNodeVisitor;
use Rector\BetterPhpDocParser\ValueObject\Parser\BetterTokenIterator;
use Rector\BetterPhpDocParser\ValueObject\PhpDoc\DoctrineAnnotation\CurlyListNode;
use Rector\BetterPhpDocParser\ValueObject\PhpDocAttributeKey;
use Rector\BetterPhpDocParser\ValueObject\Type\BracketsAwareUnionTypeNode;
use Rector\ChangesReporting\Collector\RectorChangeCollector;
@ -123,9 +124,13 @@ final class PhpDocInfo
*/
public function getTagsByName(string $name): array
{
$name = $this->annotationNaming->normalizeName($name);
// for simple tag names only
if (str_contains($name, '\\')) {
return [];
}
$tags = $this->phpDocNode->getTags();
$name = $this->annotationNaming->normalizeName($name);
$tags = array_filter($tags, fn (PhpDocTagNode $tag) => $tag->name === $name);
@ -218,12 +223,12 @@ final class PhpDocInfo
}
/**
* @param string[] $classes
* @param class-string[] $classes
*/
public function getByAnnotationClasses(array $classes): ?DoctrineAnnotationTagValueNode
{
foreach ($classes as $class) {
$tagValueNode = $this->getByAnnotationClass($class);
$tagValueNode = $this->findOneByAnnotationClass($class);
if ($tagValueNode instanceof DoctrineAnnotationTagValueNode) {
return $tagValueNode;
}
@ -232,9 +237,17 @@ final class PhpDocInfo
return null;
}
/**
* @param class-string $class
*/
public function getByAnnotationClass(string $class): ?DoctrineAnnotationTagValueNode
{
return $this->findOneByAnnotationClass($class);
}
public function hasByAnnotationClass(string $class): bool
{
return $this->getByAnnotationClass($class) !== null;
return $this->findByAnnotationClass($class) !== [];
}
/**
@ -245,42 +258,22 @@ final class PhpDocInfo
return $this->getByAnnotationClasses($annotationsClasses) !== null;
}
public function getByAnnotationClass(string $desiredClass): ?DoctrineAnnotationTagValueNode
/**
* @param class-string $desiredClass
*/
public function findOneByAnnotationClass(string $desiredClass): ?DoctrineAnnotationTagValueNode
{
foreach ($this->phpDocNode->children as $phpDocChildNode) {
if (! $phpDocChildNode instanceof PhpDocTagNode) {
continue;
}
$foundTagValueNodes = $this->findByAnnotationClass($desiredClass);
return $foundTagValueNodes[0] ?? null;
}
// new approach
if (! $phpDocChildNode->value instanceof DoctrineAnnotationTagValueNode) {
continue;
}
$doctrineAnnotationTagValueNode = $phpDocChildNode->value;
if ($doctrineAnnotationTagValueNode->hasClassName($desiredClass)) {
return $doctrineAnnotationTagValueNode;
}
// fnmatch
$identifierTypeNode = $doctrineAnnotationTagValueNode->identifierTypeNode;
if ($this->isFnmatch($identifierTypeNode->name, $desiredClass)) {
return $doctrineAnnotationTagValueNode;
}
// FQN check
$resolvedClass = $identifierTypeNode->getAttribute(PhpDocAttributeKey::RESOLVED_CLASS);
if (! is_string($resolvedClass)) {
continue;
}
if ($this->isFnmatch($resolvedClass, $desiredClass)) {
return $doctrineAnnotationTagValueNode;
}
}
return null;
/**
* @param class-string $desiredClass
* @return DoctrineAnnotationTagValueNode[]
*/
public function findByAnnotationClass(string $desiredClass): array
{
return $this->filterDoctrineTagValuesNodesINcludingNested($desiredClass);
}
/**
@ -556,4 +549,91 @@ final class PhpDocInfo
return fnmatch($desiredValue, $currentValue, FNM_NOESCAPE);
}
/**
* @return DoctrineAnnotationTagValueNode[]
*/
private function filterDoctrineTagValuesNodesINcludingNested(string $desiredClass): array
{
$desiredDoctrineTagValueNodes = [];
$doctrineTagValueNodes = $this->getDoctrineTagValueNodesNestedIncluded();
foreach ($doctrineTagValueNodes as $doctrineTagValueNode) {
if ($this->isMatchingDesiredClass($doctrineTagValueNode, $desiredClass)) {
$desiredDoctrineTagValueNodes[] = $doctrineTagValueNode;
}
}
return $desiredDoctrineTagValueNodes;
}
private function isMatchingDesiredClass(
DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode,
string $desiredClass
): bool {
if ($doctrineAnnotationTagValueNode->hasClassName($desiredClass)) {
return true;
}
$identifierTypeNode = $doctrineAnnotationTagValueNode->identifierTypeNode;
if ($this->isFnmatch($identifierTypeNode->name, $desiredClass)) {
return true;
}
// FQN check
$resolvedClass = $identifierTypeNode->getAttribute(PhpDocAttributeKey::RESOLVED_CLASS);
return is_string($resolvedClass) && $this->isFnmatch($resolvedClass, $desiredClass);
}
/**
* @return DoctrineAnnotationTagValueNode[]
*/
private function getDoctrineTagValueNodesNestedIncluded(): array
{
$doctrineTagValueNodes = [];
foreach ($this->phpDocNode->children as $phpDocChildNode) {
if (! $phpDocChildNode instanceof PhpDocTagNode) {
continue;
}
if (! $phpDocChildNode->value instanceof DoctrineAnnotationTagValueNode) {
continue;
}
$doctrineTagValueNodes[] = $phpDocChildNode->value;
}
// search nested tags too
$nestedDoctrineTagValueNodes = $this->resolveNestedDoctrineTagValueNodes($doctrineTagValueNodes);
return array_merge($doctrineTagValueNodes, $nestedDoctrineTagValueNodes);
}
/**
* @param DoctrineAnnotationTagValueNode[] $doctrineTagValueNodes
* @return DoctrineAnnotationTagValueNode[]
*/
private function resolveNestedDoctrineTagValueNodes(array $doctrineTagValueNodes): array
{
$nestedDoctrineAnnotationTagValueNodes = [];
foreach ($doctrineTagValueNodes as $doctrineTagValueNode) {
foreach ($doctrineTagValueNode->getValues() as $nestedTagValue) {
if (! $nestedTagValue instanceof CurlyListNode) {
continue;
}
foreach ($nestedTagValue->getValues() as $nestedTagValueNestedValue) {
if (! $nestedTagValueNestedValue instanceof DoctrineAnnotationTagValueNode) {
continue;
}
$nestedDoctrineAnnotationTagValueNodes[] = $nestedTagValueNestedValue;
}
}
}
return $nestedDoctrineAnnotationTagValueNodes;
}
}

View File

@ -35,7 +35,9 @@ final class PhpDocClassRenamer
*/
private function processAssertChoiceTagValueNode(array $oldToNewClasses, PhpDocInfo $phpDocInfo): void
{
$assertChoiceTagValueNode = $phpDocInfo->getByAnnotationClass('Symfony\Component\Validator\Constraints\Choice');
$assertChoiceTagValueNode = $phpDocInfo->findOneByAnnotationClass(
'Symfony\Component\Validator\Constraints\Choice'
);
if (! $assertChoiceTagValueNode instanceof DoctrineAnnotationTagValueNode) {
return;
}
@ -83,7 +85,7 @@ final class PhpDocClassRenamer
*/
private function processSerializerTypeTagValueNode(array $oldToNewClasses, PhpDocInfo $phpDocInfo): void
{
$doctrineAnnotationTagValueNode = $phpDocInfo->getByAnnotationClass('JMS\Serializer\Annotation\Type');
$doctrineAnnotationTagValueNode = $phpDocInfo->findOneByAnnotationClass('JMS\Serializer\Annotation\Type');
if (! $doctrineAnnotationTagValueNode instanceof DoctrineAnnotationTagValueNode) {
return;
}

View File

@ -529,3 +529,10 @@ parameters:
message: '#Do not use @method tag in class docblock#'
paths:
- rules/*/Enum/*
# class-string miss match
- '#Parameter \#1 \$classes of method Rector\\BetterPhpDocParser\\PhpDocInfo\\PhpDocInfo<PHPStan\\PhpDocParser\\Ast\\Node\>\:\:getByAnnotationClasses\(\) expects array<class\-string\>, array<int, string\> given#'
- '#Parameter \#1 \$classes of method Rector\\BetterPhpDocParser\\PhpDocInfo\\PhpDocInfo<TNode of PHPStan\\PhpDocParser\\Ast\\Node\>\:\:getByAnnotationClasses\(\) expects array<class\-string\>, array<string\> given#'
- '#Cognitive complexity for "Rector\\BetterPhpDocParser\\PhpDocInfo\\PhpDocInfo\:\:resolveNestedDoctrineTagValueNodes\(\)" is 11, keep it under 9#'

View File

@ -0,0 +1,42 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Annotation\ArrayWrapper;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
final class NestedUnwrap
{
/**
* @ArrayWrapper({
* @GenericAnnotation("yes")
* })
*/
public function action()
{
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Annotation\ArrayWrapper;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
final class NestedUnwrap
{
/**
* @ArrayWrapper({
* @GenericAnnotation("yes")
* })
*/
#[\Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation('yes')]
public function action()
{
}
}
?>

View File

@ -9,8 +9,8 @@ class News
/**
* @var integer
*
* @ORM\Column(name="id", type="bigint", nullable=false)
* @ORM\Id
* @ORM\Column(name="id", type="bigint", nullable=false)
*/
private $id;
}
@ -21,8 +21,8 @@ class News
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\FixtureAutoImported;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping as ORM;
class News
@ -30,8 +30,8 @@ class News
/**
* @var integer
*/
#[Column(name: 'id', type: 'bigint', nullable: false)]
#[Id]
#[Column(name: 'id', type: 'bigint', nullable: false)]
private $id;
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Annotation;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
/**
* @annotation
*/
final class ArrayWrapper
{
/**
* @param GenericAnnotation[] $genericAnnotations
*/
public function __construct(
private array $genericAnnotations
) {
}
/**
* @return GenericAnnotation[]
*/
public function getGenericAnnotations(): array
{
return $this->genericAnnotations;
}
}

View File

@ -12,9 +12,7 @@ use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Property;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover;
use Rector\Core\Contract\Rector\ConfigurableRectorInterface;
@ -117,10 +115,17 @@ CODE_SAMPLE
return null;
}
$tags = $phpDocInfo->getAllTags();
$originalAttrGroupsCount = count($node->attrGroups);
$hasNewAttrGroups = $this->processApplyAttrGroups($tags, $phpDocInfo, $node);
if ($hasNewAttrGroups) {
// 1. generic tags
$this->processGenericTags($phpDocInfo, $node);
// 2. Doctrine annotation classes
$this->processDoctrineAnnotationClasses($phpDocInfo, $node);
$currentAttrGroupsCount = count($node->attrGroups);
// something has changed
if ($originalAttrGroupsCount !== $currentAttrGroupsCount) {
return $node;
}
@ -138,52 +143,6 @@ CODE_SAMPLE
$this->annotationsToAttributes = $annotationsToAttributes;
}
/**
* @param array<PhpDocTagNode> $tags
*/
private function processApplyAttrGroups(
array $tags,
PhpDocInfo $phpDocInfo,
Class_ | Property | ClassMethod | Function_ | Closure | ArrowFunction $node
): bool {
$hasNewAttrGroups = false;
foreach ($tags as $tag) {
foreach ($this->annotationsToAttributes as $annotationToAttribute) {
$annotationToAttributeTag = $annotationToAttribute->getTag();
if ($this->isFoundGenericTag($phpDocInfo, $tag->value, $annotationToAttributeTag)) {
// 1. remove php-doc tag
$this->phpDocTagRemover->removeByName($phpDocInfo, $annotationToAttributeTag);
// 2. add attributes
$node->attrGroups[] = $this->phpAttributeGroupFactory->createFromSimpleTag(
$annotationToAttribute
);
$hasNewAttrGroups = true;
continue 2;
}
if ($this->shouldSkip($tag->value, $phpDocInfo, $annotationToAttributeTag)) {
continue;
}
// 1. remove php-doc tag
$this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $tag->value);
// 2. add attributes
/** @var DoctrineAnnotationTagValueNode $tagValue */
$tagValue = $tag->value;
$node->attrGroups[] = $this->phpAttributeGroupFactory->create($tagValue, $annotationToAttribute);
$hasNewAttrGroups = true;
continue 2;
}
}
return $hasNewAttrGroups;
}
private function isFoundGenericTag(
PhpDocInfo $phpDocInfo,
PhpDocTagValueNode $phpDocTagValueNode,
@ -192,19 +151,54 @@ CODE_SAMPLE
if (! $phpDocInfo->hasByName($annotationToAttributeTag)) {
return false;
}
return $phpDocTagValueNode instanceof GenericTagValueNode;
}
private function shouldSkip(
PhpDocTagValueNode $phpDocTagValueNode,
private function processGenericTags(
PhpDocInfo $phpDocInfo,
string $annotationToAttributeTag
): bool {
$doctrineAnnotationTagValueNode = $phpDocInfo->getByAnnotationClass($annotationToAttributeTag);
if ($phpDocTagValueNode !== $doctrineAnnotationTagValueNode) {
return true;
}
ClassMethod | Function_ | Closure | ArrowFunction | Property | Class_ $node
): void {
foreach ($phpDocInfo->getAllTags() as $phpDocTagNode) {
foreach ($this->annotationsToAttributes as $annotationToAttribute) {
$desiredTag = $annotationToAttribute->getTag();
return ! $phpDocTagValueNode instanceof DoctrineAnnotationTagValueNode;
// not a basic one
if (str_contains($desiredTag, '\\')) {
continue;
}
if (! $this->isFoundGenericTag($phpDocInfo, $phpDocTagNode->value, $desiredTag)) {
continue;
}
// 1. remove php-doc tag
$this->phpDocTagRemover->removeByName($phpDocInfo, $desiredTag);
// 2. add attributes
$node->attrGroups[] = $this->phpAttributeGroupFactory->createFromSimpleTag($annotationToAttribute);
}
}
}
private function processDoctrineAnnotationClasses(
PhpDocInfo $phpDocInfo,
ClassMethod | Function_ | Closure | ArrowFunction | Property | Class_ $node
): void {
foreach ($this->annotationsToAttributes as $annotationToAttribute) {
$doctrineAnnotationTagValueNodes = $phpDocInfo->findByAnnotationClass($annotationToAttribute->getTag());
foreach ($doctrineAnnotationTagValueNodes as $doctrineAnnotationTagValueNode) {
// 1. remove php-doc tag
$this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $doctrineAnnotationTagValueNode);
// 2. add attributes
$node->attrGroups[] = $this->phpAttributeGroupFactory->create(
$doctrineAnnotationTagValueNode,
$annotationToAttribute
);
}
}
}
}

View File

@ -136,10 +136,9 @@ CODE_SAMPLE
continue;
}
$requiredDoctrineAnnotationTagValueNode = $propertyPhpDocInfo->getByAnnotationClass(
$requiredDoctrineAnnotationTagValueNode = $propertyPhpDocInfo->findOneByAnnotationClass(
'Doctrine\Common\Annotations\Annotation\Required'
);
if (! $requiredDoctrineAnnotationTagValueNode instanceof DoctrineAnnotationTagValueNode) {
continue;
}
@ -195,7 +194,7 @@ CODE_SAMPLE
private function decorateTarget(PhpDocInfo $phpDocInfo, AttributeGroup $attributeGroup): void
{
$targetDoctrineAnnotationTagValueNode = $phpDocInfo->getByAnnotationClass(
$targetDoctrineAnnotationTagValueNode = $phpDocInfo->findOneByAnnotationClass(
'Doctrine\Common\Annotations\Annotation\Target'
);

View File

@ -79,7 +79,7 @@ final class DoctrineColumnPropertyTypeInferer implements PropertyTypeInfererInte
{
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property);
$doctrineAnnotationTagValueNode = $phpDocInfo->getByAnnotationClass('Doctrine\ORM\Mapping\Column');
$doctrineAnnotationTagValueNode = $phpDocInfo->findOneByAnnotationClass('Doctrine\ORM\Mapping\Column');
if (! $doctrineAnnotationTagValueNode instanceof DoctrineAnnotationTagValueNode) {
return null;
}

View File

@ -52,7 +52,9 @@ final class DoctrineRelationPropertyTypeInferer implements PropertyTypeInfererIn
]);
if ($toOneRelationTagValueNode !== null) {
$joinDoctrineAnnotationTagValueNode = $phpDocInfo->getByAnnotationClass('Doctrine\ORM\Mapping\JoinColumn');
$joinDoctrineAnnotationTagValueNode = $phpDocInfo->findOneByAnnotationClass(
'Doctrine\ORM\Mapping\JoinColumn'
);
return $this->processToOneRelation(
$property,