From 2b51b257e110ea850610ade8a01340e8a757295a Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 30 Aug 2019 20:21:21 +0200 Subject: [PATCH 1/2] decouple PhpDocClassRenamer --- src/PhpDoc/PhpDocClassRenamer.php | 47 +++++++++++++++++ src/Rector/Class_/RenameClassRector.php | 51 +++++-------------- .../RenameClassRectorTest.php | 30 +++++------ 3 files changed, 74 insertions(+), 54 deletions(-) create mode 100644 src/PhpDoc/PhpDocClassRenamer.php diff --git a/src/PhpDoc/PhpDocClassRenamer.php b/src/PhpDoc/PhpDocClassRenamer.php new file mode 100644 index 000000000000..2ce67ae9154d --- /dev/null +++ b/src/PhpDoc/PhpDocClassRenamer.php @@ -0,0 +1,47 @@ +getDocComment(); + if ($docComment === null) { + return; + } + + $textDocComment = $docComment->getText(); + + $oldTypes = array_keys($oldToNewClasses); + + $oldTypesPregQuoted = []; + foreach ($oldTypes as $oldType) { + $oldTypesPregQuoted[] = '\b' . preg_quote($oldType) . '\b'; + } + + $oldTypesPattern = '#(?|' . implode('|', $oldTypesPregQuoted) . ')#x'; + + $match = Strings::match($textDocComment, $oldTypesPattern); + if ($match === null) { + return; + } + + foreach ($match as $matchedOldType) { + $newType = $oldToNewClasses[$matchedOldType]; + $textDocComment = Strings::replace($textDocComment, '#\b' . preg_quote($matchedOldType) . '\b#', $newType); + } + + $node->setDocComment(new Doc($textDocComment)); + } +} diff --git a/src/Rector/Class_/RenameClassRector.php b/src/Rector/Class_/RenameClassRector.php index 891ecd0e94af..633e50a9f0e0 100644 --- a/src/Rector/Class_/RenameClassRector.php +++ b/src/Rector/Class_/RenameClassRector.php @@ -2,8 +2,6 @@ namespace Rector\Rector\Class_; -use Nette\Utils\Strings; -use PhpParser\Comment\Doc; use PhpParser\Node; use PhpParser\Node\Expr\New_; use PhpParser\Node\FunctionLike; @@ -20,6 +18,7 @@ use Rector\CodingStyle\Naming\ClassNaming; use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator; +use Rector\PhpDoc\PhpDocClassRenamer; use Rector\Rector\AbstractRector; use Rector\RectorDefinition\ConfiguredCodeSample; use Rector\RectorDefinition\RectorDefinition; @@ -46,17 +45,24 @@ final class RenameClassRector extends AbstractRector */ private $classNaming; + /** + * @var PhpDocClassRenamer + */ + private $phpDocClassRenamer; + /** * @param string[] $oldToNewClasses */ public function __construct( DocBlockManipulator $docBlockManipulator, ClassNaming $classNaming, + PhpDocClassRenamer $phpDocClassRenamer, array $oldToNewClasses = [] ) { $this->docBlockManipulator = $docBlockManipulator; $this->classNaming = $classNaming; $this->oldToNewClasses = $oldToNewClasses; + $this->phpDocClassRenamer = $phpDocClassRenamer; } public function getDefinition(): RectorDefinition @@ -90,7 +96,9 @@ function someFunction(SomeNewClass $someOldClass): SomeNewClass CODE_SAMPLE , [ - 'App\SomeOldClass' => 'App\SomeNewClass', + '$oldToNewClasses' => [ + 'App\SomeOldClass' => 'App\SomeNewClass', + ], ] ), ]); @@ -123,7 +131,7 @@ public function refactor(Node $node): ?Node } } - $this->changeTypeInAnnotationTypes($node); + $this->phpDocClassRenamer->changeTypeInAnnotationTypes($node, $this->oldToNewClasses); if ($node instanceof Name) { return $this->refactorName($node); @@ -301,39 +309,4 @@ private function refactorName(Node $node): ?Name return new FullyQualified($newName); } - - /** - * Covers annotations like @ORM, @Serializer, @Assert etc - * See https://github.com/rectorphp/rector/issues/1872 - */ - private function changeTypeInAnnotationTypes(Node $node): void - { - $docComment = $node->getDocComment(); - if ($docComment === null) { - return; - } - - $textDocComment = $docComment->getText(); - - $oldTypes = array_keys($this->oldToNewClasses); - - $oldTypesPregQuoted = []; - foreach ($oldTypes as $oldType) { - $oldTypesPregQuoted[] = '\b' . preg_quote($oldType) . '\b'; - } - - $oldTypesPattern = '#(?|' . implode('|', $oldTypesPregQuoted) . ')#x'; - - $match = Strings::match($textDocComment, $oldTypesPattern); - if ($match === null) { - return; - } - - foreach ($match as $matchedOldType) { - $newType = $this->oldToNewClasses[$matchedOldType]; - $textDocComment = Strings::replace($textDocComment, '#\b' . preg_quote($matchedOldType) . '\b#', $newType); - } - - $node->setDocComment(new Doc($textDocComment)); - } } diff --git a/tests/Rector/Class_/RenameClassRector/RenameClassRectorTest.php b/tests/Rector/Class_/RenameClassRector/RenameClassRectorTest.php index 81869338168a..0e708f851439 100644 --- a/tests/Rector/Class_/RenameClassRector/RenameClassRectorTest.php +++ b/tests/Rector/Class_/RenameClassRector/RenameClassRectorTest.php @@ -28,21 +28,21 @@ public function test(string $filePath): void public function provideTestFiles(): Iterator { - yield [__DIR__ . '/Fixture/class_to_new.php.inc']; - yield [__DIR__ . '/Fixture/class_to_interface.php.inc']; - yield [__DIR__ . '/Fixture/interface_to_class.php.inc']; - yield [__DIR__ . '/Fixture/name_insensitive.php.inc']; - yield [__DIR__ . '/Fixture/twig_case.php.inc']; - yield [__DIR__ . '/Fixture/underscore_doc.php.inc']; - yield [__DIR__ . '/Fixture/keep_return_tag.php.inc']; - - // Renaming class itself and its namespace - yield [__DIR__ . '/Fixture/rename_class_without_namespace.php.inc']; - yield [__DIR__ . '/Fixture/rename_class.php.inc']; - yield [__DIR__ . '/Fixture/rename_interface.php.inc']; - yield [__DIR__ . '/Fixture/rename_trait.php.inc']; - yield [__DIR__ . '/Fixture/rename_class_without_namespace_to_class_without_namespace.php.inc']; - yield [__DIR__ . '/Fixture/rename_class_to_class_without_namespace.php.inc']; +// yield [__DIR__ . '/Fixture/class_to_new.php.inc']; +// yield [__DIR__ . '/Fixture/class_to_interface.php.inc']; +// yield [__DIR__ . '/Fixture/interface_to_class.php.inc']; +// yield [__DIR__ . '/Fixture/name_insensitive.php.inc']; +// yield [__DIR__ . '/Fixture/twig_case.php.inc']; +// yield [__DIR__ . '/Fixture/underscore_doc.php.inc']; +// yield [__DIR__ . '/Fixture/keep_return_tag.php.inc']; +// +// // Renaming class itself and its namespace +// yield [__DIR__ . '/Fixture/rename_class_without_namespace.php.inc']; +// yield [__DIR__ . '/Fixture/rename_class.php.inc']; +// yield [__DIR__ . '/Fixture/rename_interface.php.inc']; +// yield [__DIR__ . '/Fixture/rename_trait.php.inc']; +// yield [__DIR__ . '/Fixture/rename_class_without_namespace_to_class_without_namespace.php.inc']; +// yield [__DIR__ . '/Fixture/rename_class_to_class_without_namespace.php.inc']; // Symfony/Validator + Doctrine + JMS/Serializer annotations yield [__DIR__ . '/Fixture/class_annotations.php.inc']; From 04d01ffdfe8f284375d3c785fcf0a0e653e8ae2a Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 30 Aug 2019 21:41:21 +0200 Subject: [PATCH 2/2] add Symfony Validator annotations --- ...veRepositoryFromEntityAnnotationRector.php | 2 +- .../src/PhpDocInfo/PhpDocInfo.php | 44 +-- .../Ast/PhpDoc/AbstractTagValueNode.php | 51 ++++ .../src/PhpDocParser/BetterPhpDocParser.php | 2 +- .../PhpDocParser/DoctrineDocBlockResolver.php | 8 +- .../AddUuidToEntityWhereMissingRector.php | 4 +- .../AnnotationReader/NodeAnnotationReader.php | 8 +- .../PhpDoc/AbstractDoctrineTagValueNode.php | 39 --- .../Property_/ManyToManyTagValueNode.php | 5 + .../Property_/ManyToOneTagValueNode.php | 5 + .../Property_/OneToManyTagValueNode.php | 5 + .../PhpDoc/Property_/OneToOneTagValueNode.php | 5 + .../DoctrineRelationTagValueNodeInterface.php | 2 + .../src/PhpDocParser/OrmTagParser.php | 18 +- .../TemplateAnnotationRector.php | 2 +- .../PhpDoc/AbstractConstaintTagValueNode.php | 9 + .../Ast/PhpDoc/AssertChoiceTagValueNode.php | 63 +++++ .../Ast/PhpDoc/SerializerTypeTagValueNode.php | 49 ++++ .../SymfonyPhpDocParserExtension.php | 32 +++ .../PhpDocParser/SymfonyPhpDocTagParser.php | 61 ++++ .../DoctrineColumnPropertyTypeInferer.php | 2 +- src/PhpDoc/PhpDocClassRenamer.php | 102 ++++++- .../Node/Commander/NodeAddingCommander.php | 1 - ...viceGetterToConstructorInjectionRector.php | 2 - stubs/JMS/Serializer/Type.php | 20 ++ stubs/Symfony/Validator/Choice.php | 42 +++ stubs/Symfony/Validator/Constraint.php | 262 ++++++++++++++++++ .../Fixture/class_annotations.php.inc | 12 - .../class_annotations_serializer_type.php.inc | 31 +++ .../RenameClassRectorTest.php | 31 ++- 30 files changed, 794 insertions(+), 125 deletions(-) create mode 100644 packages/Symfony/src/PhpDocParser/Ast/PhpDoc/AbstractConstaintTagValueNode.php create mode 100644 packages/Symfony/src/PhpDocParser/Ast/PhpDoc/AssertChoiceTagValueNode.php create mode 100644 packages/Symfony/src/PhpDocParser/Ast/PhpDoc/SerializerTypeTagValueNode.php create mode 100644 packages/Symfony/src/PhpDocParser/Extension/SymfonyPhpDocParserExtension.php create mode 100644 packages/Symfony/src/PhpDocParser/SymfonyPhpDocTagParser.php create mode 100644 stubs/JMS/Serializer/Type.php create mode 100644 stubs/Symfony/Validator/Choice.php create mode 100644 stubs/Symfony/Validator/Constraint.php create mode 100644 tests/Rector/Class_/RenameClassRector/Fixture/class_annotations_serializer_type.php.inc diff --git a/packages/Architecture/src/Rector/Class_/RemoveRepositoryFromEntityAnnotationRector.php b/packages/Architecture/src/Rector/Class_/RemoveRepositoryFromEntityAnnotationRector.php index cedd54e5138c..ea9ad06705a9 100644 --- a/packages/Architecture/src/Rector/Class_/RemoveRepositoryFromEntityAnnotationRector.php +++ b/packages/Architecture/src/Rector/Class_/RemoveRepositoryFromEntityAnnotationRector.php @@ -73,7 +73,7 @@ public function refactor(Node $node): ?Node $phpDocInfo = $this->docBlockManipulator->createPhpDocInfoFromNode($node); - $doctrineEntityTag = $phpDocInfo->getDoctrineEntityTag(); + $doctrineEntityTag = $phpDocInfo->getDoctrineEntity(); if ($doctrineEntityTag === null) { return null; } diff --git a/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfo.php b/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfo.php index d988d2aa8e17..546ae6bba4f6 100644 --- a/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfo.php +++ b/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfo.php @@ -177,49 +177,49 @@ public function getVarTypes(): array return $this->getResolvedTypesAttribute($varTagValue); } - public function getDoctrineIdTagValueNode(): ?IdTagValueNode + public function getDoctrineId(): ?IdTagValueNode { - return $this->matchChildValueNodeOfType(IdTagValueNode::class); + return $this->getByType(IdTagValueNode::class); } - public function getDoctrineTableTagValueNode(): ?TableTagValueNode + public function getDoctrineTable(): ?TableTagValueNode { - return $this->matchChildValueNodeOfType(TableTagValueNode::class); + return $this->getByType(TableTagValueNode::class); } - public function getDoctrineManyToManyTagValueNode(): ?ManyToManyTagValueNode + public function getDoctrineManyToMany(): ?ManyToManyTagValueNode { - return $this->matchChildValueNodeOfType(ManyToManyTagValueNode::class); + return $this->getByType(ManyToManyTagValueNode::class); } - public function getDoctrineManyToOneTagValueNode(): ?ManyToOneTagValueNode + public function getDoctrineManyToOne(): ?ManyToOneTagValueNode { - return $this->matchChildValueNodeOfType(ManyToOneTagValueNode::class); + return $this->getByType(ManyToOneTagValueNode::class); } - public function getDoctrineOneToOneTagValueNode(): ?OneToOneTagValueNode + public function getDoctrineOneToOne(): ?OneToOneTagValueNode { - return $this->matchChildValueNodeOfType(OneToOneTagValueNode::class); + return $this->getByType(OneToOneTagValueNode::class); } - public function getDoctrineOneToManyTagValueNode(): ?OneToManyTagValueNode + public function getDoctrineOneToMany(): ?OneToManyTagValueNode { - return $this->matchChildValueNodeOfType(OneToManyTagValueNode::class); + return $this->getByType(OneToManyTagValueNode::class); } - public function getDoctrineEntityTag(): ?EntityTagValueNode + public function getDoctrineEntity(): ?EntityTagValueNode { - return $this->matchChildValueNodeOfType(EntityTagValueNode::class); + return $this->getByType(EntityTagValueNode::class); } - public function getDoctrineColumnTagValueNode(): ?ColumnTagValueNode + public function getDoctrineColumn(): ?ColumnTagValueNode { - return $this->matchChildValueNodeOfType(ColumnTagValueNode::class); + return $this->getByType(ColumnTagValueNode::class); } public function getDoctrineJoinColumnTagValueNode(): ?JoinColumnTagValueNode { - return $this->matchChildValueNodeOfType(JoinColumnTagValueNode::class); + return $this->getByType(JoinColumnTagValueNode::class); } /** @@ -263,10 +263,10 @@ public function getReturnTypes(): array public function getDoctrineRelationTagValueNode(): ?DoctrineRelationTagValueNodeInterface { - return $this->getDoctrineManyToManyTagValueNode() ?? - $this->getDoctrineOneToManyTagValueNode() ?? - $this->getDoctrineOneToOneTagValueNode() ?? - $this->getDoctrineManyToOneTagValueNode() ?? null; + return $this->getDoctrineManyToMany() ?? + $this->getDoctrineOneToMany() ?? + $this->getDoctrineOneToOne() ?? + $this->getDoctrineManyToOne() ?? null; } public function removeTagValueNodeFromNode(PhpDocTagValueNode $phpDocTagValueNode): void @@ -285,7 +285,7 @@ public function removeTagValueNodeFromNode(PhpDocTagValueNode $phpDocTagValueNod /** * @param string $type */ - public function matchChildValueNodeOfType(string $type): ?PhpDocTagValueNode + public function getByType(string $type): ?PhpDocTagValueNode { foreach ($this->phpDocNode->children as $phpDocChildNode) { if ($phpDocChildNode instanceof PhpDocTagNode) { diff --git a/packages/BetterPhpDocParser/src/PhpDocParser/Ast/PhpDoc/AbstractTagValueNode.php b/packages/BetterPhpDocParser/src/PhpDocParser/Ast/PhpDoc/AbstractTagValueNode.php index b25129f29e77..31c4b7365a1c 100644 --- a/packages/BetterPhpDocParser/src/PhpDocParser/Ast/PhpDoc/AbstractTagValueNode.php +++ b/packages/BetterPhpDocParser/src/PhpDocParser/Ast/PhpDoc/AbstractTagValueNode.php @@ -7,11 +7,17 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode; use Rector\BetterPhpDocParser\Attributes\Attribute\AttributeTrait; use Rector\BetterPhpDocParser\Attributes\Contract\Ast\AttributeAwareNodeInterface; +use Rector\DoctrinePhpDocParser\Array_\ArrayItemStaticHelper; abstract class AbstractTagValueNode implements AttributeAwareNodeInterface, PhpDocTagValueNode { use AttributeTrait; + /** + * @var string[]|null + */ + protected $orderedVisibleItems; + /** * @param mixed[] $item */ @@ -21,6 +27,51 @@ protected function printArrayItem(array $item, string $key): string $json = Strings::replace($json, '#,#', ', '); $json = Strings::replace($json, '#\[(.*?)\]#', '{$1}'); + // cleanup json encoded extra slashes + $json = Strings::replace($json, '#\\\\\\\\#', '\\'); + return sprintf('%s=%s', $key, $json); } + + /** + * @param string[] $contentItems + */ + protected function printContentItems(array $contentItems): string + { + if ($this->orderedVisibleItems !== null) { + $contentItems = ArrayItemStaticHelper::filterAndSortVisibleItems($contentItems, $this->orderedVisibleItems); + } + + if ($contentItems === []) { + return ''; + } + + return '(' . implode(', ', $contentItems) . ')'; + } + + /** + * @param PhpDocTagValueNode[] $tagValueNodes + */ + protected function printTagValueNodesSeparatedByComma(array $tagValueNodes, string $prefix = ''): string + { + if ($tagValueNodes === []) { + return ''; + } + + $itemsAsStrings = []; + foreach ($tagValueNodes as $tagValueNode) { + $itemsAsStrings[] = $prefix . (string) $tagValueNode; + } + + return implode(', ', $itemsAsStrings); + } + + protected function resolveItemsOrderFromAnnotationContent(?string $annotationContent): void + { + if ($annotationContent === null) { + return; + } + + $this->orderedVisibleItems = ArrayItemStaticHelper::resolveAnnotationItemsOrder($annotationContent); + } } diff --git a/packages/BetterPhpDocParser/src/PhpDocParser/BetterPhpDocParser.php b/packages/BetterPhpDocParser/src/PhpDocParser/BetterPhpDocParser.php index c55dbdbec73f..69bdb6e2813c 100644 --- a/packages/BetterPhpDocParser/src/PhpDocParser/BetterPhpDocParser.php +++ b/packages/BetterPhpDocParser/src/PhpDocParser/BetterPhpDocParser.php @@ -119,7 +119,7 @@ public function parseTag(TokenIterator $tokenIterator): PhpDocTagNode $tokenIterator->next(); // @todo somehow decouple to tag pre-processor - if ($tag === '@ORM') { + if (Strings::match($tag, '#@(ORM|Assert|Serializer)$#')) { $tag .= $tokenIterator->currentTokenValue(); $tokenIterator->next(); } diff --git a/packages/Doctrine/src/PhpDocParser/DoctrineDocBlockResolver.php b/packages/Doctrine/src/PhpDocParser/DoctrineDocBlockResolver.php index d191ea633c29..dabf74a44a90 100644 --- a/packages/Doctrine/src/PhpDocParser/DoctrineDocBlockResolver.php +++ b/packages/Doctrine/src/PhpDocParser/DoctrineDocBlockResolver.php @@ -29,7 +29,7 @@ public function isDoctrineEntityClass(Class_ $class): bool return false; } - return (bool) $classPhpDocInfo->getDoctrineEntityTag(); + return (bool) $classPhpDocInfo->getDoctrineEntity(); } public function getTargetEntity(Property $property): ?string @@ -49,7 +49,7 @@ public function hasPropertyDoctrineIdTag(Property $property): bool return false; } - return (bool) $propertyPhpDocInfo->getDoctrineIdTagValueNode(); + return (bool) $propertyPhpDocInfo->getDoctrineId(); } public function getDoctrineRelationTagValueNode(Property $property): ?DoctrineRelationTagValueNodeInterface @@ -69,7 +69,7 @@ public function getDoctrineTableTagValueNode(Class_ $class): ?TableTagValueNode return null; } - return $classPhpDocInfo->getDoctrineTableTagValueNode(); + return $classPhpDocInfo->getDoctrineTable(); } public function isDoctrineProperty(Property $property): bool @@ -79,7 +79,7 @@ public function isDoctrineProperty(Property $property): bool return false; } - if ($propertyPhpDocInfo->getDoctrineColumnTagValueNode()) { + if ($propertyPhpDocInfo->getDoctrineColumn()) { return true; } diff --git a/packages/Doctrine/src/Rector/Class_/AddUuidToEntityWhereMissingRector.php b/packages/Doctrine/src/Rector/Class_/AddUuidToEntityWhereMissingRector.php index 646fe8c51d01..0a3073caf617 100644 --- a/packages/Doctrine/src/Rector/Class_/AddUuidToEntityWhereMissingRector.php +++ b/packages/Doctrine/src/Rector/Class_/AddUuidToEntityWhereMissingRector.php @@ -116,13 +116,13 @@ private function hasClassIdPropertyWithUuidType(Class_ $class): bool return false; } - $idTagValueNode = $propertyPhpDocInfo->getDoctrineIdTagValueNode(); + $idTagValueNode = $propertyPhpDocInfo->getDoctrineId(); if ($idTagValueNode === null) { return false; } // get column! - $columnTagValueNode = $propertyPhpDocInfo->getDoctrineColumnTagValueNode(); + $columnTagValueNode = $propertyPhpDocInfo->getDoctrineColumn(); if ($columnTagValueNode === null) { return false; } diff --git a/packages/DoctrinePhpDocParser/src/AnnotationReader/NodeAnnotationReader.php b/packages/DoctrinePhpDocParser/src/AnnotationReader/NodeAnnotationReader.php index 0ea01f676923..113cdf66d4d5 100644 --- a/packages/DoctrinePhpDocParser/src/AnnotationReader/NodeAnnotationReader.php +++ b/packages/DoctrinePhpDocParser/src/AnnotationReader/NodeAnnotationReader.php @@ -14,6 +14,7 @@ use ReflectionMethod; use ReflectionProperty; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; +use Symfony\Component\Validator\Constraint; final class NodeAnnotationReader { @@ -46,7 +47,7 @@ public function readMethodAnnotation(ClassMethod $classMethod, string $annotatio return $this->annotationReader->getMethodAnnotation($reflectionMethod, $annotationClassName); } - public function readDoctrineClassAnnotation(Class_ $class, string $annotationClassName): Annotation + public function readClassAnnotation(Class_ $class, string $annotationClassName): Annotation { $classReflection = $this->createClassReflectionFromNode($class); @@ -59,7 +60,10 @@ public function readDoctrineClassAnnotation(Class_ $class, string $annotationCla return $classAnnotation; } - public function readDoctrinePropertyAnnotation(Property $property, string $annotationClassName): Annotation + /** + * @return Annotation|Constraint|null + */ + public function readPropertyAnnotation(Property $property, string $annotationClassName) { $propertyReflection = $this->createPropertyReflectionFromPropertyNode($property); diff --git a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/AbstractDoctrineTagValueNode.php b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/AbstractDoctrineTagValueNode.php index 5d284cdb2abe..cf095181ae78 100644 --- a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/AbstractDoctrineTagValueNode.php +++ b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/AbstractDoctrineTagValueNode.php @@ -2,48 +2,9 @@ namespace Rector\DoctrinePhpDocParser\Ast\PhpDoc; -use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode; use Rector\BetterPhpDocParser\PhpDocParser\Ast\PhpDoc\AbstractTagValueNode; -use Rector\DoctrinePhpDocParser\Array_\ArrayItemStaticHelper; use Rector\DoctrinePhpDocParser\Contract\Ast\PhpDoc\DoctrineTagNodeInterface; abstract class AbstractDoctrineTagValueNode extends AbstractTagValueNode implements DoctrineTagNodeInterface { - /** - * @var string[]|null - */ - protected $orderedVisibleItems; - - /** - * @param string[] $contentItems - */ - protected function printContentItems(array $contentItems): string - { - if ($this->orderedVisibleItems !== null) { - $contentItems = ArrayItemStaticHelper::filterAndSortVisibleItems($contentItems, $this->orderedVisibleItems); - } - - if ($contentItems === []) { - return ''; - } - - return '(' . implode(', ', $contentItems) . ')'; - } - - /** - * @param PhpDocTagValueNode[] $tagValueNodes - */ - protected function printTagValueNodesSeparatedByComma(array $tagValueNodes, string $prefix = ''): string - { - if ($tagValueNodes === []) { - return ''; - } - - $itemsAsStrings = []; - foreach ($tagValueNodes as $tagValueNode) { - $itemsAsStrings[] = $prefix . (string) $tagValueNode; - } - - return implode(', ', $itemsAsStrings); - } } diff --git a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/ManyToManyTagValueNode.php b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/ManyToManyTagValueNode.php index c51d3dd1e4fc..7dbfde871529 100644 --- a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/ManyToManyTagValueNode.php +++ b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/ManyToManyTagValueNode.php @@ -133,4 +133,9 @@ public function removeInversedBy(): void { $this->inversedBy = null; } + + public function changeTargetEntity(string $targetEntity): void + { + $this->targetEntity = $targetEntity; + } } diff --git a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/ManyToOneTagValueNode.php b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/ManyToOneTagValueNode.php index 8ce6303bf237..f0612a5340aa 100644 --- a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/ManyToOneTagValueNode.php +++ b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/ManyToOneTagValueNode.php @@ -90,4 +90,9 @@ public function removeInversedBy(): void { $this->inversedBy = null; } + + public function changeTargetEntity(string $targetEntity): void + { + $this->targetEntity = $targetEntity; + } } diff --git a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/OneToManyTagValueNode.php b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/OneToManyTagValueNode.php index 092c14e1b1de..abdd907038e8 100644 --- a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/OneToManyTagValueNode.php +++ b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/OneToManyTagValueNode.php @@ -107,4 +107,9 @@ public function removeMappedBy(): void { $this->mappedBy = null; } + + public function changeTargetEntity(string $targetEntity): void + { + $this->targetEntity = $targetEntity; + } } diff --git a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/OneToOneTagValueNode.php b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/OneToOneTagValueNode.php index f05f4a5d90db..b0cf4e2b98dc 100644 --- a/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/OneToOneTagValueNode.php +++ b/packages/DoctrinePhpDocParser/src/Ast/PhpDoc/Property_/OneToOneTagValueNode.php @@ -126,4 +126,9 @@ public function removeMappedBy(): void { $this->mappedBy = null; } + + public function changeTargetEntity(string $targetEntity): void + { + $this->targetEntity = $targetEntity; + } } diff --git a/packages/DoctrinePhpDocParser/src/Contract/Ast/PhpDoc/DoctrineRelationTagValueNodeInterface.php b/packages/DoctrinePhpDocParser/src/Contract/Ast/PhpDoc/DoctrineRelationTagValueNodeInterface.php index 7068c93ac76c..3047b47a8f47 100644 --- a/packages/DoctrinePhpDocParser/src/Contract/Ast/PhpDoc/DoctrineRelationTagValueNodeInterface.php +++ b/packages/DoctrinePhpDocParser/src/Contract/Ast/PhpDoc/DoctrineRelationTagValueNodeInterface.php @@ -7,4 +7,6 @@ interface DoctrineRelationTagValueNodeInterface public function getTargetEntity(): ?string; public function getFqnTargetEntity(): ?string; + + public function changeTargetEntity(string $targetEntity): void; } diff --git a/packages/DoctrinePhpDocParser/src/PhpDocParser/OrmTagParser.php b/packages/DoctrinePhpDocParser/src/PhpDocParser/OrmTagParser.php index 1d9f2a739e17..5a8386ec2073 100644 --- a/packages/DoctrinePhpDocParser/src/PhpDocParser/OrmTagParser.php +++ b/packages/DoctrinePhpDocParser/src/PhpDocParser/OrmTagParser.php @@ -113,7 +113,7 @@ private function createIdTagValueNode(): IdTagValueNode private function createEntityTagValueNode(Class_ $class, string $annotationContent): EntityTagValueNode { /** @var Entity $entity */ - $entity = $this->nodeAnnotationReader->readDoctrineClassAnnotation($class, Entity::class); + $entity = $this->nodeAnnotationReader->readClassAnnotation($class, Entity::class); return new EntityTagValueNode($entity->repositoryClass, $entity->readOnly, $this->resolveAnnotationItemsOrder( $annotationContent @@ -123,7 +123,7 @@ private function createEntityTagValueNode(Class_ $class, string $annotationConte private function createTableTagValueNode(Class_ $class, string $annotationContent): TableTagValueNode { /** @var Table $table */ - $table = $this->nodeAnnotationReader->readDoctrineClassAnnotation($class, Table::class); + $table = $this->nodeAnnotationReader->readClassAnnotation($class, Table::class); return new TableTagValueNode( $table->name, @@ -138,7 +138,7 @@ private function createTableTagValueNode(Class_ $class, string $annotationConten private function createColumnTagValueNode(Property $property, string $annotationContent): ColumnTagValueNode { /** @var Column $column */ - $column = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, Column::class); + $column = $this->nodeAnnotationReader->readPropertyAnnotation($property, Column::class); return new ColumnTagValueNode( $column->name, @@ -157,7 +157,7 @@ private function createColumnTagValueNode(Property $property, string $annotation private function createManyToManyTagValueNode(Property $property, string $annotationContent): ManyToManyTagValueNode { /** @var ManyToMany $manyToMany */ - $manyToMany = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, ManyToMany::class); + $manyToMany = $this->nodeAnnotationReader->readPropertyAnnotation($property, ManyToMany::class); return new ManyToManyTagValueNode( $manyToMany->targetEntity, @@ -175,7 +175,7 @@ private function createManyToManyTagValueNode(Property $property, string $annota private function createManyToOneTagValueNode(Property $property, string $annotationContent): ManyToOneTagValueNode { /** @var ManyToOne $manyToOne */ - $manyToOne = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, ManyToOne::class); + $manyToOne = $this->nodeAnnotationReader->readPropertyAnnotation($property, ManyToOne::class); return new ManyToOneTagValueNode( $manyToOne->targetEntity, @@ -190,7 +190,7 @@ private function createManyToOneTagValueNode(Property $property, string $annotat private function createOneToOneTagValueNode(Property $property, string $annotationContent): OneToOneTagValueNode { /** @var OneToOne $oneToOne */ - $oneToOne = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, OneToOne::class); + $oneToOne = $this->nodeAnnotationReader->readPropertyAnnotation($property, OneToOne::class); return new OneToOneTagValueNode( $oneToOne->targetEntity, @@ -207,7 +207,7 @@ private function createOneToOneTagValueNode(Property $property, string $annotati private function createOneToManyTagValueNode(Property $property, string $annotationContent): OneToManyTagValueNode { /** @var OneToMany $oneToMany */ - $oneToMany = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, OneToMany::class); + $oneToMany = $this->nodeAnnotationReader->readPropertyAnnotation($property, OneToMany::class); return new OneToManyTagValueNode( $oneToMany->mappedBy, @@ -224,7 +224,7 @@ private function createOneToManyTagValueNode(Property $property, string $annotat private function createJoinColumnTagValueNode(Property $property, string $annotationContent): JoinColumnTagValueNode { /** @var JoinColumn $joinColumn */ - $joinColumn = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, JoinColumn::class); + $joinColumn = $this->nodeAnnotationReader->readPropertyAnnotation($property, JoinColumn::class); return $this->createJoinColumnTagValueNodeFromJoinColumnAnnotation($joinColumn, $annotationContent); } @@ -232,7 +232,7 @@ private function createJoinColumnTagValueNode(Property $property, string $annota private function createJoinTableTagValeNode(Property $property, string $annotationContent): JoinTableTagValueNode { /** @var JoinTable $joinTable */ - $joinTable = $this->nodeAnnotationReader->readDoctrinePropertyAnnotation($property, JoinTable::class); + $joinTable = $this->nodeAnnotationReader->readPropertyAnnotation($property, JoinTable::class); $joinColumnContents = Strings::matchAll( $annotationContent, diff --git a/packages/Sensio/src/Rector/FrameworkExtraBundle/TemplateAnnotationRector.php b/packages/Sensio/src/Rector/FrameworkExtraBundle/TemplateAnnotationRector.php index 93fcc6f54e4e..397c87564062 100644 --- a/packages/Sensio/src/Rector/FrameworkExtraBundle/TemplateAnnotationRector.php +++ b/packages/Sensio/src/Rector/FrameworkExtraBundle/TemplateAnnotationRector.php @@ -135,7 +135,7 @@ private function resolveTemplateName(ClassMethod $classMethod): string $classMethodPhpDocInfo = $this->getPhpDocInfo($classMethod); /** @var TemplateTagValueNode|null $templateTagValueNode */ - $templateTagValueNode = $classMethodPhpDocInfo->matchChildValueNodeOfType(TemplateTagValueNode::class); + $templateTagValueNode = $classMethodPhpDocInfo->getByType(TemplateTagValueNode::class); if ($templateTagValueNode === null) { throw new ShouldNotHappenException(__METHOD__); } diff --git a/packages/Symfony/src/PhpDocParser/Ast/PhpDoc/AbstractConstaintTagValueNode.php b/packages/Symfony/src/PhpDocParser/Ast/PhpDoc/AbstractConstaintTagValueNode.php new file mode 100644 index 000000000000..00a312c98ef9 --- /dev/null +++ b/packages/Symfony/src/PhpDocParser/Ast/PhpDoc/AbstractConstaintTagValueNode.php @@ -0,0 +1,9 @@ +callback = $callback; + $this->strict = $strict; + $this->resolveItemsOrderFromAnnotationContent($annotationContent); + } + + public function __toString(): string + { + $contentItems = []; + + if ($this->callback) { + $contentItems['callback'] = $this->printArrayItem($this->callback, 'callback'); + } + + if ($this->strict !== null) { + $contentItems['strict'] = sprintf('strict=%s', $this->strict ? 'true' : 'false'); + } + + return $this->printContentItems($contentItems); + } + + public function isCallbackClass(string $class): bool + { + return $class === ($this->callback[0] ?? null); + } + + public function changeCallbackClass(string $newClass): void + { + $this->callback[0] = $newClass; + } +} diff --git a/packages/Symfony/src/PhpDocParser/Ast/PhpDoc/SerializerTypeTagValueNode.php b/packages/Symfony/src/PhpDocParser/Ast/PhpDoc/SerializerTypeTagValueNode.php new file mode 100644 index 000000000000..0482a1483ee3 --- /dev/null +++ b/packages/Symfony/src/PhpDocParser/Ast/PhpDoc/SerializerTypeTagValueNode.php @@ -0,0 +1,49 @@ +name = $name; + $this->resolveItemsOrderFromAnnotationContent($annotationContent); + } + + public function __toString(): string + { + return sprintf('("%s")', $this->name); + } + + public function replaceName(string $oldName, string $newName): bool + { + $oldNamePattern = '#\b' . preg_quote($oldName, '#') . '\b#'; + + $newNameValue = Strings::replace($this->name, $oldNamePattern, $newName); + if ($newNameValue !== $this->name) { + $this->name = $newNameValue; + return true; + } + + return false; + } +} diff --git a/packages/Symfony/src/PhpDocParser/Extension/SymfonyPhpDocParserExtension.php b/packages/Symfony/src/PhpDocParser/Extension/SymfonyPhpDocParserExtension.php new file mode 100644 index 000000000000..d1bb652c2fa3 --- /dev/null +++ b/packages/Symfony/src/PhpDocParser/Extension/SymfonyPhpDocParserExtension.php @@ -0,0 +1,32 @@ +symfonyPhpDocTagParser = $symfonyPhpDocTagParser; + } + + public function matchTag(string $tag): bool + { + return (bool) Strings::match($tag, '#^@(Assert|Serializer)\\\\(.*?)$#'); + } + + public function parse(TokenIterator $tokenIterator, string $tag): ?PhpDocTagValueNode + { + return $this->symfonyPhpDocTagParser->parse($tokenIterator, $tag); + } +} diff --git a/packages/Symfony/src/PhpDocParser/SymfonyPhpDocTagParser.php b/packages/Symfony/src/PhpDocParser/SymfonyPhpDocTagParser.php new file mode 100644 index 000000000000..758c87a74c5e --- /dev/null +++ b/packages/Symfony/src/PhpDocParser/SymfonyPhpDocTagParser.php @@ -0,0 +1,61 @@ +getCurrentPhpNode(); + + // this is needed to append tokens to the end of annotation, even if not used + $annotationContent = $this->resolveAnnotationContent($tokenIterator); + + if ($currentPhpNode instanceof Property) { + if ($tag === AssertChoiceTagValueNode::SHORT_NAME) { + return $this->createAssertChoiceTagValueNode($currentPhpNode, $annotationContent); + } + + if ($tag === SerializerTypeTagValueNode::SHORT_NAME) { + return $this->createSerializerTypeTagValueNode($currentPhpNode, $annotationContent); + } + } + + return null; + } + + private function createAssertChoiceTagValueNode( + Property $property, + string $annotationContent + ): AssertChoiceTagValueNode { + /** @var Choice $choiceAnnotation */ + $choiceAnnotation = $this->nodeAnnotationReader->readPropertyAnnotation( + $property, + AssertChoiceTagValueNode::CLASS_NAME + ); + + return new AssertChoiceTagValueNode($choiceAnnotation->callback, $choiceAnnotation->strict, $annotationContent); + } + + private function createSerializerTypeTagValueNode( + Property $property, + string $annotationContent + ): SerializerTypeTagValueNode { + /** @var Type $typeAnnotation */ + $typeAnnotation = $this->nodeAnnotationReader->readPropertyAnnotation( + $property, + SerializerTypeTagValueNode::CLASS_NAME + ); + + return new SerializerTypeTagValueNode($typeAnnotation->name, $annotationContent); + } +} diff --git a/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/DoctrineColumnPropertyTypeInferer.php b/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/DoctrineColumnPropertyTypeInferer.php index b410bb8a79c1..a68b4f37ffb7 100644 --- a/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/DoctrineColumnPropertyTypeInferer.php +++ b/packages/TypeDeclaration/src/TypeInferer/PropertyTypeInferer/DoctrineColumnPropertyTypeInferer.php @@ -68,7 +68,7 @@ public function inferProperty(Property $property): array } $phpDocInfo = $this->docBlockManipulator->createPhpDocInfoFromNode($property); - $doctrineColumnTagValueNode = $phpDocInfo->getDoctrineColumnTagValueNode(); + $doctrineColumnTagValueNode = $phpDocInfo->getDoctrineColumn(); if ($doctrineColumnTagValueNode === null) { return []; } diff --git a/src/PhpDoc/PhpDocClassRenamer.php b/src/PhpDoc/PhpDocClassRenamer.php index 2ce67ae9154d..3488404a0dfc 100644 --- a/src/PhpDoc/PhpDocClassRenamer.php +++ b/src/PhpDoc/PhpDocClassRenamer.php @@ -2,12 +2,38 @@ namespace Rector\PhpDoc; -use Nette\Utils\Strings; use PhpParser\Comment\Doc; use PhpParser\Node; +use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; +use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; +use Rector\BetterPhpDocParser\Printer\PhpDocInfoPrinter; +use Rector\DoctrinePhpDocParser\Contract\Ast\PhpDoc\DoctrineRelationTagValueNodeInterface; +use Rector\Symfony\PhpDocParser\Ast\PhpDoc\AssertChoiceTagValueNode; +use Rector\Symfony\PhpDocParser\Ast\PhpDoc\SerializerTypeTagValueNode; final class PhpDocClassRenamer { + /** + * @var PhpDocInfoFactory + */ + private $phpDocInfoFactory; + + /** + * @var PhpDocInfoPrinter + */ + private $phpDocInfoPrinter; + + /** + * @var bool + */ + private $shouldUpdate = false; + + public function __construct(PhpDocInfoFactory $phpDocInfoFactory, PhpDocInfoPrinter $phpDocInfoPrinter) + { + $this->phpDocInfoFactory = $phpDocInfoFactory; + $this->phpDocInfoPrinter = $phpDocInfoPrinter; + } + /** * Covers annotations like @ORM, @Serializer, @Assert etc * See https://github.com/rectorphp/rector/issues/1872 @@ -21,27 +47,77 @@ public function changeTypeInAnnotationTypes(Node $node, array $oldToNewClasses): return; } - $textDocComment = $docComment->getText(); + $this->shouldUpdate = false; - $oldTypes = array_keys($oldToNewClasses); + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + $this->procesAssertChoiceTagValueNode($oldToNewClasses, $phpDocInfo); + $this->procesDoctrineRelationTagValueNode($oldToNewClasses, $phpDocInfo); + $this->processSerializerTypeTagValueNode($oldToNewClasses, $phpDocInfo); - $oldTypesPregQuoted = []; - foreach ($oldTypes as $oldType) { - $oldTypesPregQuoted[] = '\b' . preg_quote($oldType) . '\b'; + if ($this->shouldUpdate === null) { + return; } - $oldTypesPattern = '#(?|' . implode('|', $oldTypesPregQuoted) . ')#x'; + $textDocComment = $this->phpDocInfoPrinter->printFormatPreserving($phpDocInfo); + $node->setDocComment(new Doc($textDocComment)); + } - $match = Strings::match($textDocComment, $oldTypesPattern); - if ($match === null) { + /** + * @param string[] $oldToNewClasses + */ + private function procesAssertChoiceTagValueNode(array $oldToNewClasses, PhpDocInfo $phpDocInfo): void + { + $choiceTagValueNode = $phpDocInfo->getByType(AssertChoiceTagValueNode::class); + if (! $choiceTagValueNode instanceof AssertChoiceTagValueNode) { return; } - foreach ($match as $matchedOldType) { - $newType = $oldToNewClasses[$matchedOldType]; - $textDocComment = Strings::replace($textDocComment, '#\b' . preg_quote($matchedOldType) . '\b#', $newType); + foreach ($oldToNewClasses as $oldClass => $newClass) { + if (! $choiceTagValueNode->isCallbackClass($oldClass)) { + continue; + } + + $choiceTagValueNode->changeCallbackClass($newClass); + $this->shouldUpdate = true; + break; } + } - $node->setDocComment(new Doc($textDocComment)); + /** + * @param string[] $oldToNewClasses + */ + private function procesDoctrineRelationTagValueNode(array $oldToNewClasses, PhpDocInfo $phpDocInfo): void + { + $relationTagValueNode = $phpDocInfo->getByType(DoctrineRelationTagValueNodeInterface::class); + if (! $relationTagValueNode instanceof DoctrineRelationTagValueNodeInterface) { + return; + } + + foreach ($oldToNewClasses as $oldClass => $newClass) { + if ($relationTagValueNode->getFqnTargetEntity() !== $oldClass) { + continue; + } + + $relationTagValueNode->changeTargetEntity($newClass); + $this->shouldUpdate = true; + break; + } + } + + /** + * @param string[] $oldToNewClasses + */ + private function processSerializerTypeTagValueNode(array $oldToNewClasses, PhpDocInfo $phpDocInfo): void + { + $serializerTypeTagValueNode = $phpDocInfo->getByType(SerializerTypeTagValueNode::class); + if (! $serializerTypeTagValueNode instanceof SerializerTypeTagValueNode) { + return; + } + + foreach ($oldToNewClasses as $oldClass => $newClass) { + if ($serializerTypeTagValueNode->replaceName($oldClass, $newClass)) { + $this->shouldUpdate = true; + } + } } } diff --git a/src/PhpParser/Node/Commander/NodeAddingCommander.php b/src/PhpParser/Node/Commander/NodeAddingCommander.php index b9516dfc99cc..cb876c06eda3 100644 --- a/src/PhpParser/Node/Commander/NodeAddingCommander.php +++ b/src/PhpParser/Node/Commander/NodeAddingCommander.php @@ -52,7 +52,6 @@ public function addNodeBeforeNode(Node $addedNode, Node $positionNode): void { $position = $this->resolveNearestExpressionPosition($positionNode); - // dump($this->wrapToExpression($addedNode)); $this->nodesToAddBefore[$position][] = $this->wrapToExpression($addedNode); } diff --git a/src/Rector/MethodCall/ServiceGetterToConstructorInjectionRector.php b/src/Rector/MethodCall/ServiceGetterToConstructorInjectionRector.php index acd5f38bd1a2..58b915b66ad5 100644 --- a/src/Rector/MethodCall/ServiceGetterToConstructorInjectionRector.php +++ b/src/Rector/MethodCall/ServiceGetterToConstructorInjectionRector.php @@ -150,8 +150,6 @@ public function refactor(Node $node): ?Node $propertyName = $this->propertyNaming->fqnToVariableName($serviceType); - // add property via constructor - /** @var Class_ $classNode */ $this->addPropertyToClass($classNode, $serviceType, $propertyName); diff --git a/stubs/JMS/Serializer/Type.php b/stubs/JMS/Serializer/Type.php new file mode 100644 index 000000000000..f88bc0c7c458 --- /dev/null +++ b/stubs/JMS/Serializer/Type.php @@ -0,0 +1,20 @@ + 'NO_SUCH_CHOICE_ERROR', + self::TOO_FEW_ERROR => 'TOO_FEW_ERROR', + self::TOO_MANY_ERROR => 'TOO_MANY_ERROR', + ]; + public $choices; + public $callback; + public $multiple = false; + public $strict = true; + public $min; + public $max; + public $message = 'The value you selected is not a valid choice.'; + public $multipleMessage = 'One or more of the given values is invalid.'; + public $minMessage = 'You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.'; + public $maxMessage = 'You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.'; + /** + * {@inheritdoc} + */ + public function getDefaultOption() + { + return 'choices'; + } +} diff --git a/stubs/Symfony/Validator/Constraint.php b/stubs/Symfony/Validator/Constraint.php new file mode 100644 index 000000000000..21ffe6d71284 --- /dev/null +++ b/stubs/Symfony/Validator/Constraint.php @@ -0,0 +1,262 @@ +getDefaultOption(); + $invalidOptions = []; + $missingOptions = array_flip((array) $this->getRequiredOptions()); + $knownOptions = get_object_vars($this); + + // The "groups" option is added to the object lazily + $knownOptions['groups'] = true; + + if (\is_array($options) && isset($options['value']) && !property_exists($this, 'value')) { + if (null === $defaultOption) { + throw new Exception(sprintf('No default option is configured for constraint "%s".', \get_class($this))); + } + + $options[$defaultOption] = $options['value']; + unset($options['value']); + } + + if (\is_array($options)) { + reset($options); + } + if ($options && \is_array($options) && \is_string(key($options))) { + foreach ($options as $option => $value) { + if (\array_key_exists($option, $knownOptions)) { + $this->$option = $value; + unset($missingOptions[$option]); + } else { + $invalidOptions[] = $option; + } + } + } elseif (null !== $options && !(\is_array($options) && 0 === \count($options))) { + if (null === $defaultOption) { + throw new Exception(sprintf('No default option is configured for constraint "%s".', \get_class($this))); + } + + if (\array_key_exists($defaultOption, $knownOptions)) { + $this->$defaultOption = $options; + unset($missingOptions[$defaultOption]); + } else { + $invalidOptions[] = $defaultOption; + } + } + + if (\count($invalidOptions) > 0) { + throw new InvalidOptionsException(sprintf('The options "%s" do not exist in constraint "%s".', implode('", "', $invalidOptions), \get_class($this)), $invalidOptions); + } + + if (\count($missingOptions) > 0) { + throw new MissingOptionsException(sprintf('The options "%s" must be set for constraint "%s".', implode('", "', array_keys($missingOptions)), \get_class($this)), array_keys($missingOptions)); + } + } + + /** + * Sets the value of a lazily initialized option. + * + * Corresponding properties are added to the object on first access. Hence + * this method will be called at most once per constraint instance and + * option name. + * + * @param string $option The option name + * @param mixed $value The value to set + * + * @throws InvalidOptionsException If an invalid option name is given + */ + public function __set($option, $value) + { + if ('groups' === $option) { + $this->groups = (array) $value; + + return; + } + + throw new InvalidOptionsException(sprintf('The option "%s" does not exist in constraint "%s".', $option, \get_class($this)), [$option]); + } + + /** + * Returns the value of a lazily initialized option. + * + * Corresponding properties are added to the object on first access. Hence + * this method will be called at most once per constraint instance and + * option name. + * + * @param string $option The option name + * + * @return mixed The value of the option + * + * @throws InvalidOptionsException If an invalid option name is given + * + * @internal this method should not be used or overwritten in userland code + */ + public function __get($option) + { + if ('groups' === $option) { + $this->groups = [self::DEFAULT_GROUP]; + + return $this->groups; + } + + throw new InvalidOptionsException(sprintf('The option "%s" does not exist in constraint "%s".', $option, \get_class($this)), [$option]); + } + + /** + * @param string $option The option name + * + * @return bool + */ + public function __isset($option) + { + return 'groups' === $option; + } + + /** + * Adds the given group if this constraint is in the Default group. + * + * @param string $group + */ + public function addImplicitGroupName($group) + { + if (\in_array(self::DEFAULT_GROUP, $this->groups) && !\in_array($group, $this->groups)) { + $this->groups[] = $group; + } + } + + /** + * Returns the name of the default option. + * + * Override this method to define a default option. + * + * @return string|null + * + * @see __construct() + */ + public function getDefaultOption() + { + return null; + } + + /** + * Returns the name of the required options. + * + * Override this method if you want to define required options. + * + * @return array + * + * @see __construct() + */ + public function getRequiredOptions() + { + return []; + } + + /** + * Returns the name of the class that validates this constraint. + * + * By default, this is the fully qualified name of the constraint class + * suffixed with "Validator". You can override this method to change that + * behavior. + * + * @return string + */ + public function validatedBy() + { + return \get_class($this).'Validator'; + } + + /** + * Returns whether the constraint can be put onto classes, properties or + * both. + * + * This method should return one or more of the constants + * Constraint::CLASS_CONSTRAINT and Constraint::PROPERTY_CONSTRAINT. + * + * @return string|array One or more constant values + */ + public function getTargets() + { + return self::PROPERTY_CONSTRAINT; + } + + /** + * Optimizes the serialized value to minimize storage space. + * + * @internal + */ + public function __sleep(): array + { + // Initialize "groups" option if it is not set + $this->groups; + + return array_keys(get_object_vars($this)); + } +} diff --git a/tests/Rector/Class_/RenameClassRector/Fixture/class_annotations.php.inc b/tests/Rector/Class_/RenameClassRector/Fixture/class_annotations.php.inc index e3e646c980c1..c7b0aa1af122 100644 --- a/tests/Rector/Class_/RenameClassRector/Fixture/class_annotations.php.inc +++ b/tests/Rector/Class_/RenameClassRector/Fixture/class_annotations.php.inc @@ -23,12 +23,6 @@ class ClassAnnotations */ public $keepThis; - /** - * @Serializer\Type("array") - * @Serializer\Type("iterable") - */ - public $flights = []; - /** * @ORM\OneToMany(targetEntity="Rector\Tests\Rector\Class_\RenameClassRector\Source\OldClass") */ @@ -62,12 +56,6 @@ class ClassAnnotations */ public $keepThis; - /** - * @Serializer\Type("array") - * @Serializer\Type("iterable") - */ - public $flights = []; - /** * @ORM\OneToMany(targetEntity="Rector\Tests\Rector\Class_\RenameClassRector\Source\NewClass") */ diff --git a/tests/Rector/Class_/RenameClassRector/Fixture/class_annotations_serializer_type.php.inc b/tests/Rector/Class_/RenameClassRector/Fixture/class_annotations_serializer_type.php.inc new file mode 100644 index 000000000000..ce5b5907bf08 --- /dev/null +++ b/tests/Rector/Class_/RenameClassRector/Fixture/class_annotations_serializer_type.php.inc @@ -0,0 +1,31 @@ +") + */ + public $flights = []; +} + +?> +----- +") + */ + public $flights = []; +} + +?> diff --git a/tests/Rector/Class_/RenameClassRector/RenameClassRectorTest.php b/tests/Rector/Class_/RenameClassRector/RenameClassRectorTest.php index 0e708f851439..462a43e2d8af 100644 --- a/tests/Rector/Class_/RenameClassRector/RenameClassRectorTest.php +++ b/tests/Rector/Class_/RenameClassRector/RenameClassRectorTest.php @@ -28,24 +28,25 @@ public function test(string $filePath): void public function provideTestFiles(): Iterator { -// yield [__DIR__ . '/Fixture/class_to_new.php.inc']; -// yield [__DIR__ . '/Fixture/class_to_interface.php.inc']; -// yield [__DIR__ . '/Fixture/interface_to_class.php.inc']; -// yield [__DIR__ . '/Fixture/name_insensitive.php.inc']; -// yield [__DIR__ . '/Fixture/twig_case.php.inc']; -// yield [__DIR__ . '/Fixture/underscore_doc.php.inc']; -// yield [__DIR__ . '/Fixture/keep_return_tag.php.inc']; -// -// // Renaming class itself and its namespace -// yield [__DIR__ . '/Fixture/rename_class_without_namespace.php.inc']; -// yield [__DIR__ . '/Fixture/rename_class.php.inc']; -// yield [__DIR__ . '/Fixture/rename_interface.php.inc']; -// yield [__DIR__ . '/Fixture/rename_trait.php.inc']; -// yield [__DIR__ . '/Fixture/rename_class_without_namespace_to_class_without_namespace.php.inc']; -// yield [__DIR__ . '/Fixture/rename_class_to_class_without_namespace.php.inc']; + yield [__DIR__ . '/Fixture/class_to_new.php.inc']; + yield [__DIR__ . '/Fixture/class_to_interface.php.inc']; + yield [__DIR__ . '/Fixture/interface_to_class.php.inc']; + yield [__DIR__ . '/Fixture/name_insensitive.php.inc']; + yield [__DIR__ . '/Fixture/twig_case.php.inc']; + yield [__DIR__ . '/Fixture/underscore_doc.php.inc']; + yield [__DIR__ . '/Fixture/keep_return_tag.php.inc']; + + // Renaming class itself and its namespace + yield [__DIR__ . '/Fixture/rename_class_without_namespace.php.inc']; + yield [__DIR__ . '/Fixture/rename_class.php.inc']; + yield [__DIR__ . '/Fixture/rename_interface.php.inc']; + yield [__DIR__ . '/Fixture/rename_trait.php.inc']; + yield [__DIR__ . '/Fixture/rename_class_without_namespace_to_class_without_namespace.php.inc']; + yield [__DIR__ . '/Fixture/rename_class_to_class_without_namespace.php.inc']; // Symfony/Validator + Doctrine + JMS/Serializer annotations yield [__DIR__ . '/Fixture/class_annotations.php.inc']; + yield [__DIR__ . '/Fixture/class_annotations_serializer_type.php.inc']; } /**