phpAttributeGroupFactory = $phpAttributeGroupFactory; $this->attrGroupsFactory = $attrGroupsFactory; $this->phpDocTagRemover = $phpDocTagRemover; $this->attributeGroupNamedArgumentManipulator = $attributeGroupNamedArgumentManipulator; $this->useImportsResolver = $useImportsResolver; $this->phpAttributeAnalyzer = $phpAttributeAnalyzer; $this->docBlockUpdater = $docBlockUpdater; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change annotation to attribute', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' use Symfony\Component\Routing\Annotation\Route; class SymfonyRoute { /** * @Route("/path", name="action") api route */ public function action() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' use Symfony\Component\Routing\Annotation\Route; class SymfonyRoute { #[Route(path: '/path', name: 'action')] // api route public function action() { } } CODE_SAMPLE , [new AnnotationToAttribute('Symfony\\Component\\Routing\\Annotation\\Route')])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class, Property::class, Param::class, ClassMethod::class, Function_::class, Closure::class, ArrowFunction::class, Interface_::class]; } /** * @param Class_|Property|Param|ClassMethod|Function_|Closure|ArrowFunction|Interface_ $node */ public function refactor(Node $node) : ?Node { if ($this->annotationsToAttributes === []) { throw new InvalidConfigurationException(\sprintf('The "%s" rule requires configuration.', self::class)); } $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); if (!$phpDocInfo instanceof PhpDocInfo) { return null; } $uses = $this->useImportsResolver->resolveBareUses(); // 1. bare tags without annotation class, e.g. "@require" $genericAttributeGroups = $this->processGenericTags($phpDocInfo); // 2. Doctrine annotation classes $annotationAttributeGroups = $this->processDoctrineAnnotationClasses($phpDocInfo, $uses); $attributeGroups = \array_merge($genericAttributeGroups, $annotationAttributeGroups); if ($attributeGroups === []) { return null; } // 3. Reprint docblock $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); $this->attributeGroupNamedArgumentManipulator->decorate($attributeGroups); $node->attrGroups = \array_merge($node->attrGroups, $attributeGroups); return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, AnnotationToAttribute::class); $this->annotationsToAttributes = $configuration; } public function provideMinPhpVersion() : int { return PhpVersionFeature::ATTRIBUTES; } /** * @return AttributeGroup[] */ private function processGenericTags(PhpDocInfo $phpDocInfo) : array { $attributeGroups = []; $phpDocNodeTraverser = new PhpDocNodeTraverser(); $phpDocNodeTraverser->traverseWithCallable($phpDocInfo->getPhpDocNode(), '', function (DocNode $docNode) use(&$attributeGroups) : ?int { if (!$docNode instanceof PhpDocTagNode) { return null; } if (!$docNode->value instanceof GenericTagValueNode) { return null; } $tag = \trim($docNode->name, '@'); // not a basic one if (\strpos($tag, '\\') !== \false) { return null; } foreach ($this->annotationsToAttributes as $annotationToAttribute) { $desiredTag = $annotationToAttribute->getTag(); if ($desiredTag !== $tag) { continue; } $attributeGroups[] = $this->phpAttributeGroupFactory->createFromSimpleTag($annotationToAttribute); return PhpDocNodeTraverser::NODE_REMOVE; } return null; }); return $attributeGroups; } /** * @param Use_[] $uses * @return AttributeGroup[] */ private function processDoctrineAnnotationClasses(PhpDocInfo $phpDocInfo, array $uses) : array { if ($phpDocInfo->getPhpDocNode()->children === []) { return []; } $doctrineTagAndAnnotationToAttributes = []; $doctrineTagValueNodes = []; foreach ($phpDocInfo->getPhpDocNode()->children as $phpDocChildNode) { if (!$phpDocChildNode instanceof PhpDocTagNode) { continue; } if (!$phpDocChildNode->value instanceof DoctrineAnnotationTagValueNode) { continue; } $doctrineTagValueNode = $phpDocChildNode->value; $annotationToAttribute = $this->matchAnnotationToAttribute($doctrineTagValueNode); if (!$annotationToAttribute instanceof AnnotationToAttribute) { continue; } $doctrineTagAndAnnotationToAttributes[] = new DoctrineTagAndAnnotationToAttribute($doctrineTagValueNode, $annotationToAttribute); $doctrineTagValueNodes[] = $doctrineTagValueNode; } $attributeGroups = $this->attrGroupsFactory->create($doctrineTagAndAnnotationToAttributes, $uses); if ($this->phpAttributeAnalyzer->hasRemoveArrayState($attributeGroups)) { return []; } foreach ($doctrineTagValueNodes as $doctrineTagValueNode) { $this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $doctrineTagValueNode); } return $attributeGroups; } private function matchAnnotationToAttribute(DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode) : ?\Rector\Php80\ValueObject\AnnotationToAttribute { foreach ($this->annotationsToAttributes as $annotationToAttribute) { if (!$doctrineAnnotationTagValueNode->hasClassName($annotationToAttribute->getTag())) { continue; } return $annotationToAttribute; } return null; } }