From 20ff21baa0eeaa23694bb66cd6fa7864a67ab9cf Mon Sep 17 00:00:00 2001 From: Simon Schaufelberger Date: Thu, 25 Sep 2025 01:42:35 +0200 Subject: [PATCH] Add example for issue #9388 --- .../AttributeDecorator.php | 26 +++ .../AttributeDecoratorInterface.php | 14 ++ .../ValidateAttributeDecorator.php | 67 +++++++ .../Issues/Issue9388/Fixture/fixture.php.inc | 33 ++++ tests/Issues/Issue9388/Issue9388Test.php | 36 ++++ .../ExtbaseAnnotationToAttributeRector.php | 185 ++++++++++++++++++ .../Issue9388/config/configured_rule.php | 27 +++ 7 files changed, 388 insertions(+) create mode 100644 tests/Issues/Issue9388/AnnotationToAttribute/AttributeDecorator.php create mode 100644 tests/Issues/Issue9388/AnnotationToAttribute/AttributeDecoratorInterface.php create mode 100644 tests/Issues/Issue9388/AnnotationToAttribute/ValidateAttributeDecorator.php create mode 100644 tests/Issues/Issue9388/Fixture/fixture.php.inc create mode 100644 tests/Issues/Issue9388/Issue9388Test.php create mode 100644 tests/Issues/Issue9388/Rule/ExtbaseAnnotationToAttributeRector.php create mode 100644 tests/Issues/Issue9388/config/configured_rule.php diff --git a/tests/Issues/Issue9388/AnnotationToAttribute/AttributeDecorator.php b/tests/Issues/Issue9388/AnnotationToAttribute/AttributeDecorator.php new file mode 100644 index 00000000000..92674fba7a6 --- /dev/null +++ b/tests/Issues/Issue9388/AnnotationToAttribute/AttributeDecorator.php @@ -0,0 +1,26 @@ +decorators as $decorator) { + if ($decorator->supports($phpAttributeName)) { + $decorator->decorate($attribute); + } + } + } +} diff --git a/tests/Issues/Issue9388/AnnotationToAttribute/AttributeDecoratorInterface.php b/tests/Issues/Issue9388/AnnotationToAttribute/AttributeDecoratorInterface.php new file mode 100644 index 00000000000..bd4ebe11d98 --- /dev/null +++ b/tests/Issues/Issue9388/AnnotationToAttribute/AttributeDecoratorInterface.php @@ -0,0 +1,14 @@ +valueResolver = $valueResolver; + $this->stringClassNameToClassConstantRector = $stringClassNameToClassConstantRector; + } + + public function supports(string $phpAttributeName): bool + { + return $phpAttributeName === 'TYPO3\CMS\Extbase\Annotation\Validate'; + } + + public function decorate(Attribute $attribute): void + { + $newArguments = new Array_(); + + foreach ($attribute->args as $arg) { + $key = $arg->name instanceof Identifier ? new String_($arg->name->toString()) : new String_('validator'); + + if ($this->valueResolver->isValue($key, 'validator')) { + $classNameString = $this->valueResolver->getValue($arg->value); + if (! is_string($classNameString)) { + continue; + } + + $className = ltrim($classNameString, '\\'); + $classConstant = $this->stringClassNameToClassConstantRector->refactor(new String_($className)); + $value = $classConstant instanceof ClassConstFetch ? $classConstant : $arg->value; + } else { + $value = $arg->value; + } + + $newArguments->items[] = new ArrayItem($value, $key); + } + + $attribute->args = [new Arg($newArguments)]; + } +} diff --git a/tests/Issues/Issue9388/Fixture/fixture.php.inc b/tests/Issues/Issue9388/Fixture/fixture.php.inc new file mode 100644 index 00000000000..a6ebb285b9b --- /dev/null +++ b/tests/Issues/Issue9388/Fixture/fixture.php.inc @@ -0,0 +1,33 @@ + 'NotEmpty'])] + protected $name = ''; + + #[\TYPO3\CMS\Extbase\Annotation\Validate(['validator' => 'NotEmpty'])] + protected $thisWorks = ''; +} diff --git a/tests/Issues/Issue9388/Issue9388Test.php b/tests/Issues/Issue9388/Issue9388Test.php new file mode 100644 index 00000000000..0745efa91ab --- /dev/null +++ b/tests/Issues/Issue9388/Issue9388Test.php @@ -0,0 +1,36 @@ +markTestSkipped('Do not execute'); + } + + $this->doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/tests/Issues/Issue9388/Rule/ExtbaseAnnotationToAttributeRector.php b/tests/Issues/Issue9388/Rule/ExtbaseAnnotationToAttributeRector.php new file mode 100644 index 00000000000..dc616a4231f --- /dev/null +++ b/tests/Issues/Issue9388/Rule/ExtbaseAnnotationToAttributeRector.php @@ -0,0 +1,185 @@ +annotationsToAttributes = [ + new AnnotationToAttribute('TYPO3\CMS\Extbase\Annotation\Validate'), + ]; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Change annotation to attribute', [new CodeSample( + <<<'CODE_SAMPLE' +use TYPO3\CMS\Extbase\Annotation as Extbase; + +class MyEntity +{ + /** + * @Extbase\ORM\Transient() + */ + protected string $myProperty; +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use TYPO3\CMS\Extbase\Annotation as Extbase; + +class MyEntity +{ + #[Extbase\ORM\Transient()] + protected string $myProperty; +} +CODE_SAMPLE + )]); + } + + public function getNodeTypes(): array + { + return [Property::class]; + } + + /** + * @param Property $node + */ + public function refactor(Node $node): ?Node + { + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + if (! $phpDocInfo instanceof PhpDocInfo) { + return null; + } + + $uses = $this->useImportsResolver->resolveBareUses(); + $annotationAttributeGroups = $this->processDoctrineAnnotationClasses($phpDocInfo, $uses); + if ($annotationAttributeGroups === []) { + return null; + } + + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + + foreach ($annotationAttributeGroups as $attributeGroup) { + foreach ($attributeGroup->attrs as $attr) { + $phpAttributeName = $attr->name->getAttribute(AttributeKey::PHP_ATTRIBUTE_NAME); + $this->attributeDecorator->decorate($phpAttributeName, $attr); + } + } + + $node->attrGroups = \array_merge($node->attrGroups, $annotationAttributeGroups); + return $node; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::ATTRIBUTES; + } + + /** + * @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; + } + + // Fix the missing leading slash in most of the wild use cases + if (str_starts_with($doctrineTagValueNode->identifierTypeNode->name, '@TYPO3\CMS')) { + $doctrineTagValueNode->identifierTypeNode->name = str_replace( + '@TYPO3\CMS', + '@\\TYPO3\CMS', + $doctrineTagValueNode->identifierTypeNode->name + ); + } + + $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 + ): ?AnnotationToAttribute { + foreach ($this->annotationsToAttributes as $annotationToAttribute) { + if (! $doctrineAnnotationTagValueNode->hasClassName($annotationToAttribute->getTag())) { + continue; + } + + return $annotationToAttribute; + } + + return null; + } +} diff --git a/tests/Issues/Issue9388/config/configured_rule.php b/tests/Issues/Issue9388/config/configured_rule.php new file mode 100644 index 00000000000..b40682b84a8 --- /dev/null +++ b/tests/Issues/Issue9388/config/configured_rule.php @@ -0,0 +1,27 @@ +autotagInterface(AttributeDecoratorInterface::class); + $rectorConfig->singleton(ValidateAttributeDecorator::class); + $rectorConfig->when(AttributeDecorator::class)->needs('$decorators')->giveTagged( + AttributeDecoratorInterface::class + ); + + $rectorConfig->importNames(false, false); + $rectorConfig->phpVersion(PhpVersionFeature::ATTRIBUTES); + + $rectorConfig->ruleWithConfiguration(RenameClassRector::class, [ + 'TYPO3\CMS\Extbase\Mvc\Web\Request' => 'TYPO3\CMS\Extbase\Mvc\Request', + ]); + $rectorConfig->rule(ExtbaseAnnotationToAttributeRector::class); +};