diff --git a/rules/CodingStyle/Node/NameImporter.php b/rules/CodingStyle/Node/NameImporter.php index 3532a597ab9..ed8848c4bcf 100644 --- a/rules/CodingStyle/Node/NameImporter.php +++ b/rules/CodingStyle/Node/NameImporter.php @@ -138,7 +138,7 @@ private function addUseImport( FullyQualified $fullyQualified, FullyQualifiedObjectType $fullyQualifiedObjectType ): void { - if ($this->useNodesToAddCollector->hasImport($file, $fullyQualified, $fullyQualifiedObjectType)) { + if ($this->useNodesToAddCollector->hasImport($file, $fullyQualifiedObjectType)) { return; } diff --git a/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php b/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php index e40eb031bdc..152070d60e3 100644 --- a/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php +++ b/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php @@ -146,7 +146,7 @@ private function processFqnNameImport( return null; } - if ($this->shouldImport($newNode, $identifierTypeNode, $fullyQualifiedObjectType)) { + if ($this->shouldImport($file, $newNode, $identifierTypeNode, $fullyQualifiedObjectType)) { $this->useNodesToAddCollector->addUseImport($fullyQualifiedObjectType); $this->hasChanged = true; @@ -157,6 +157,7 @@ private function processFqnNameImport( } private function shouldImport( + File $file, IdentifierTypeNode $newNode, IdentifierTypeNode $identifierTypeNode, FullyQualifiedObjectType $fullyQualifiedObjectType @@ -181,7 +182,7 @@ private function shouldImport( $firstPath = Strings::before($identifierTypeNode->name, '\\' . $newNode->name); if ($firstPath === null) { - return true; + return ! $this->useNodesToAddCollector->hasImport($file, $fullyQualifiedObjectType); } if ($firstPath === '') { diff --git a/src/PostRector/Collector/UseNodesToAddCollector.php b/src/PostRector/Collector/UseNodesToAddCollector.php index 43f99fa49af..867cd822996 100644 --- a/src/PostRector/Collector/UseNodesToAddCollector.php +++ b/src/PostRector/Collector/UseNodesToAddCollector.php @@ -5,7 +5,6 @@ namespace Rector\PostRector\Collector; use PhpParser\Node\Identifier; -use PhpParser\Node\Name\FullyQualified; use Rector\Application\Provider\CurrentFileProvider; use Rector\Naming\Naming\UseImportsResolver; use Rector\StaticTypeMapper\ValueObject\Type\AliasedObjectType; @@ -86,7 +85,6 @@ public function getUseImportTypesByNode(File $file): array public function hasImport( File $file, - FullyQualified $fullyQualified, FullyQualifiedObjectType $fullyQualifiedObjectType ): bool { $useImports = $this->getUseImportTypesByNode($file); diff --git a/src/StaticTypeMapper/PhpDocParser/IdentifierPhpDocTypeMapper.php b/src/StaticTypeMapper/PhpDocParser/IdentifierPhpDocTypeMapper.php index 2d9618ccc3b..76a64dc7db9 100644 --- a/src/StaticTypeMapper/PhpDocParser/IdentifierPhpDocTypeMapper.php +++ b/src/StaticTypeMapper/PhpDocParser/IdentifierPhpDocTypeMapper.php @@ -97,8 +97,7 @@ public function mapIdentifierTypeNode(IdentifierTypeNode $identifierTypeNode, No return new UnionType($scalarTypes); } - $identifierTypeNode->name = ltrim($identifierTypeNode->name, '@'); - $objectType = new ObjectType($identifierTypeNode->name); + $objectType = new ObjectType(ltrim($identifierTypeNode->name, '@')); } $scope = $node->getAttribute(AttributeKey::SCOPE); 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..07b6c0a3445 --- /dev/null +++ b/tests/Issues/Issue9388/Issue9388Test.php @@ -0,0 +1,31 @@ +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/Source/AnnotationToAttribute/AttributeDecorator.php b/tests/Issues/Issue9388/Source/AnnotationToAttribute/AttributeDecorator.php new file mode 100644 index 00000000000..69355db3c5c --- /dev/null +++ b/tests/Issues/Issue9388/Source/AnnotationToAttribute/AttributeDecorator.php @@ -0,0 +1,26 @@ +decorators as $decorator) { + if ($decorator->supports($phpAttributeName)) { + $decorator->decorate($attribute); + } + } + } +} diff --git a/tests/Issues/Issue9388/Source/AnnotationToAttribute/AttributeDecoratorInterface.php b/tests/Issues/Issue9388/Source/AnnotationToAttribute/AttributeDecoratorInterface.php new file mode 100644 index 00000000000..77af25249fa --- /dev/null +++ b/tests/Issues/Issue9388/Source/AnnotationToAttribute/AttributeDecoratorInterface.php @@ -0,0 +1,14 @@ +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; + } + + $array->items[] = new ArrayItem($value, $key); + } + + $attribute->args = [new Arg($array)]; + } +} diff --git a/tests/Issues/Issue9388/Source/Rule/ExtbaseAnnotationToAttributeRector.php b/tests/Issues/Issue9388/Source/Rule/ExtbaseAnnotationToAttributeRector.php new file mode 100644 index 00000000000..0f145bbeefe --- /dev/null +++ b/tests/Issues/Issue9388/Source/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 $annotationAttributeGroup) { + foreach ($annotationAttributeGroup->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..9f59172b0bd --- /dev/null +++ b/tests/Issues/Issue9388/config/configured_rule.php @@ -0,0 +1,28 @@ +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); + $rectorConfig->phpVersion(PhpVersionFeature::ATTRIBUTES); +};