[PHP 8.0] Add annotation removal in case of nested (#403)

* cleanup

* cleanup empty annotation with just remove children

* handle removing of parent

* decouple AttrGroupsFactory
This commit is contained in:
Tomas Votruba 2021-07-08 14:27:19 +02:00 committed by GitHub
parent 32c0d7d1b7
commit e066024702
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 159 additions and 93 deletions

View File

@ -22,6 +22,11 @@ abstract class AbstractValuesAwareNode implements PhpDocTagValueNode
protected bool $hasChanged = false;
/**
* @var mixed[]
*/
private array $originalValues = [];
/**
* @param mixed[] $values Must be public so node traverser can go through them
*/
@ -30,6 +35,7 @@ abstract class AbstractValuesAwareNode implements PhpDocTagValueNode
protected ?string $originalContent = null,
protected ?string $silentKey = null
) {
$this->originalValues = $values;
}
public function removeValue(string $key): void
@ -158,6 +164,14 @@ abstract class AbstractValuesAwareNode implements PhpDocTagValueNode
$this->hasChanged = true;
}
/**
* @return mixed[]
*/
public function getOriginalValues(): array
{
return $this->originalValues;
}
/**
* @param mixed|string $value
* @return mixed|string

View File

@ -2,10 +2,10 @@
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Attribute\Path;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
/**
* @Path("/")
* @GenericAnnotation("/")
*/
final class FromImplicitToName
{
@ -17,9 +17,9 @@ final class FromImplicitToName
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Attribute\Path;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
#[\Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Attribute\Path(path: '/')]
#[\Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation(some: '/')]
final class FromImplicitToName
{
}

View File

@ -24,7 +24,7 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericA
final class FunctionCallInside
{
#[\Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation("is_granted('ROLE_USER')")]
#[\Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation(some: "is_granted('ROLE_USER')")]
public function action()
{
}

View File

@ -2,10 +2,10 @@
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Attribute\Path;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
/**
* @Path("
* @GenericAnnotation("
* summary: Send webcam reward
* ")
*/
@ -19,9 +19,9 @@ final class MultilineContent
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Attribute\Path;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
#[\Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Attribute\Path(path: '
#[\Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation(some: '
summary: Send webcam reward
')]
final class MultilineContent

View File

@ -28,11 +28,6 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Response
final class NestedUnwrap
{
/**
* @Responses({
* @Response(code="200", description="Contests", entity=ContestListView::class),
* })
*/
#[\Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Response(code: '200', description: 'Contests', entity: ContestListView::class)]
public function action()
{

View File

@ -24,7 +24,7 @@ use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source as Asser
final class EntityColumnAndAssertChoice
{
#[\Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation(['php5', 'php7', 'php8'])]
#[\Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation(some: ['php5', 'php7', 'php8'])]
#[\Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation(choices: ['5.0', '5.1', 'id' => '5.2'])]
#[\Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation(choices: [2, 3, 5, 7, 11, 13, 17, 19])]
public $primeNumbers;

View File

@ -2,7 +2,6 @@
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\GenericAnnotation;
use Symfony\Component\Validator\Constraints as Assert;
final class SkipUnwrapSymfonyNestedValidators
@ -11,41 +10,9 @@ final class SkipUnwrapSymfonyNestedValidators
* @var array
* @Assert\All(
* constraints={
* @GenericAnnotation("firstConstraint"),
* @GenericAnnotation("secondConstraint"),
* @Assert\NotNull()
* }
* )
*/
public $all;
/**
* @Assert\Sequentially({
* @GenericAnnotation("stepOne"),
* @GenericAnnotation("stepTwo")
* }
* )
*/
public $sequentially;
/**
* @Assert\AtLeastOneOf({
* @GenericAnnotation("thisOne"),
* @GenericAnnotation("orThisOne")
* })
*/
public $atLeastOneOf;
/**
* @var array
*
* @Assert\Collection(
* fields = {
* "email" = @GenericAnnotation("email"),
* "password" = @GenericAnnotation("password"),
* }
* )
*/
public $collection;
}
?>

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Attribute;
use Attribute;
/**
* @annotation
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
final class Path
{
public function __construct(
private string $path
) {
}
}

View File

@ -10,4 +10,7 @@ namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source;
*/
final class GenericAnnotation
{
public function __construct($some)
{
}
}

View File

@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source;
final class Post
{
}

View File

@ -22,9 +22,6 @@ return static function (ContainerConfigurator $containerConfigurator): void {
new AnnotationToAttribute(Response::class),
new AnnotationToAttribute('Symfony\Component\Routing\Annotation\Route'),
new AnnotationToAttribute(
'Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Source\Attribute\Path'
),
]),
]]);
};

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Rector\Php80\NodeFactory;
use PhpParser\Node\AttributeGroup;
use Rector\Php80\ValueObject\DoctrineTagAndAnnotationToAttribute;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
final class AttrGroupsFactory
{
public function __construct(
private PhpAttributeGroupFactory $phpAttributeGroupFactory
) {
}
/**
* @param DoctrineTagAndAnnotationToAttribute[] $doctrineTagAndAnnotationToAttributes
* @return AttributeGroup[]
*/
public function create(array $doctrineTagAndAnnotationToAttributes): array
{
$attributeGroups = [];
foreach ($doctrineTagAndAnnotationToAttributes as $doctrineTagAndAnnotationToAttribute) {
$doctrineAnnotationTagValueNode = $doctrineTagAndAnnotationToAttribute->getDoctrineAnnotationTagValueNode();
// add attributes
$attributeGroups[] = $this->phpAttributeGroupFactory->create(
$doctrineAnnotationTagValueNode,
$doctrineTagAndAnnotationToAttribute->getAnnotationToAttribute()
);
}
return $attributeGroups;
}
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Rector\Php80\PhpDocCleaner;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
use Rector\BetterPhpDocParser\ValueObject\PhpDoc\DoctrineAnnotation\CurlyListNode;
use Rector\Php80\ValueObject\AnnotationToAttribute;
use Symplify\SimplePhpDocParser\PhpDocNodeTraverser;
final class ConvertedAnnotationToAttributeParentRemover
{
/**
* @param AnnotationToAttribute[] $annotationsToAttributes
*/
public function processPhpDocNode(PhpDocNode $phpDocNode, array $annotationsToAttributes): void
{
$phpDocNodeTraverser = new PhpDocNodeTraverser();
$phpDocNodeTraverser->traverseWithCallable($phpDocNode, '', function ($node) use ($annotationsToAttributes) {
if (! $node instanceof DoctrineAnnotationTagValueNode) {
return $node;
}
// has only children of annotation to attribute? it will be removed
if ($this->detect($node, $annotationsToAttributes)) {
return PhpDocNodeTraverser::NODE_REMOVE;
}
return $node;
});
}
/**
* @param AnnotationToAttribute[] $annotationsToAttributes
*/
private function detect(
DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode,
array $annotationsToAttributes
): bool {
foreach ($doctrineAnnotationTagValueNode->getValues() as $nodeValue) {
if (! $nodeValue instanceof CurlyListNode) {
return false;
}
if (! $this->isCurlyListOfDoctrineAnnotationTagValueNodes($nodeValue, $annotationsToAttributes)) {
return false;
}
}
return true;
}
/**
* @param AnnotationToAttribute[] $annotationsToAttributes
*/
private function isCurlyListOfDoctrineAnnotationTagValueNodes(
CurlyListNode $curlyListNode,
array $annotationsToAttributes
): bool {
foreach ($curlyListNode->getOriginalValues() as $nodeValueValue) {
foreach ($annotationsToAttributes as $annotationToAttribute) {
if (! $nodeValueValue instanceof DoctrineAnnotationTagValueNode) {
return false;
}
// found it
if ($nodeValueValue->hasClassName($annotationToAttribute->getTag())) {
continue 2;
}
}
return false;
}
return true;
}
}

View File

@ -19,7 +19,8 @@ use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover;
use Rector\Core\Contract\Rector\ConfigurableRectorInterface;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Php80\PhpDocNodeVisitor\AnnotationToAttributePhpDocNodeVisitor;
use Rector\Php80\NodeFactory\AttrGroupsFactory;
use Rector\Php80\PhpDocCleaner\ConvertedAnnotationToAttributeParentRemover;
use Rector\Php80\ValueObject\AnnotationToAttribute;
use Rector\Php80\ValueObject\DoctrineTagAndAnnotationToAttribute;
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
@ -59,7 +60,8 @@ final class AnnotationToAttributeRector extends AbstractRector implements Config
public function __construct(
private PhpAttributeGroupFactory $phpAttributeGroupFactory,
private PhpDocTagRemover $phpDocTagRemover,
//private AnnotationToAttributePhpDocNodeVisitor $annotationToAttributePhpDocNodeVisitor
private ConvertedAnnotationToAttributeParentRemover $convertedAnnotationToAttributeParentRemover,
private AttrGroupsFactory $attrGroupsFactory
) {
}
@ -204,6 +206,7 @@ CODE_SAMPLE
if ($this->shouldSkip($phpDocInfo)) {
return;
}
$doctrineTagAndAnnotationToAttributes = [];
$phpDocNodeTraverser = new PhpDocNodeTraverser();
@ -234,18 +237,16 @@ CODE_SAMPLE
return $node;
});
foreach ($doctrineTagAndAnnotationToAttributes as $doctrineTagAndAnnotationToAttribute) {
$doctrineAnnotationTagValueNode = $doctrineTagAndAnnotationToAttribute->getDoctrineAnnotationTagValueNode();
// 1. remove php-doc tag
$this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $doctrineAnnotationTagValueNode);
// 2. add attributes
$node->attrGroups[] = $this->phpAttributeGroupFactory->create(
$doctrineAnnotationTagValueNode,
$doctrineTagAndAnnotationToAttribute->getAnnotationToAttribute()
);
$attrGroups = $this->attrGroupsFactory->create($doctrineTagAndAnnotationToAttributes);
if ($attrGroups === []) {
return;
}
$node->attrGroups = $attrGroups;
$this->convertedAnnotationToAttributeParentRemover->processPhpDocNode(
$phpDocInfo->getPhpDocNode(),
$this->annotationsToAttributes
);
}
private function shouldSkip(PhpDocInfo $phpDocInfo): bool