diff --git a/src/Drupal10/Rector/Deprecation/AnnotationToAttributeRector.php b/src/Drupal10/Rector/Deprecation/AnnotationToAttributeRector.php new file mode 100644 index 00000000..e34d84a9 --- /dev/null +++ b/src/Drupal10/Rector/Deprecation/AnnotationToAttributeRector.php @@ -0,0 +1,282 @@ +phpDocTagRemover = $phpDocTagRemover; + $this->docBlockUpdater = $docBlockUpdater; + $this->phpDocInfoFactory = $phpDocInfoFactory; + $this->arrayParser = $arrayParser; + $this->tokenIteratorFactory = $tokenIteratorFactory; + $this->annotationToAttributeMapper = $annotationToAttributeMapper; + } + + public function configure(array $configuration): void + { + foreach ($configuration as $value) { + if (!($value instanceof AnnotationToAttributeConfiguration)) { + throw new \InvalidArgumentException(sprintf('Each configuration item must be an instance of "%s"', DrupalIntroducedVersionConfiguration::class)); + } + } + + parent::configure($configuration); + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Change annotations with value to attribute', [new CodeSample(<<<'CODE_SAMPLE' + +namespace Drupal\Core\Action\Plugin\Action; + +use Drupal\Core\Session\AccountInterface; + +/** + * Publishes an entity. + * + * @Action( + * id = "entity:publish_action", + * action_label = @Translation("Publish"), + * deriver = "Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver", + * ) + */ +class PublishAction extends EntityActionBase { +CODE_SAMPLE + , <<<'CODE_SAMPLE' + +namespace Drupal\Core\Action\Plugin\Action; + +use Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver; +use Drupal\Core\Action\Attribute\Action; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; + +/** + * Publishes an entity. + */ +#[Action( + id: 'entity:publish_action', + action_label: new TranslatableMarkup('Publish'), + deriver: EntityPublishedActionDeriver::class +)] +class PublishAction extends EntityActionBase { +CODE_SAMPLE + )]); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + public function provideMinPhpVersion(): int + { + return PhpVersion::PHP_81; + } + + /** + * @param Class_|ClassMethod $node + */ + public function refactor(Node $node): ?Node + { + foreach ($this->configuration as $configuration) { + if ($this->rectorShouldApplyToDrupalVersion($configuration) === false) { + continue; + } + + $result = $this->refactorWithConfiguration($node, $configuration); + + // Skip if no result. + if ($result === null) { + continue; + } + + return $result; + } + + return null; + } + + /** + * @param Class_|ClassMethod $node + * @param AnnotationToAttributeConfiguration $configuration + */ + protected function refactorWithConfiguration(Node $node, VersionedConfigurationInterface $configuration): ?Node + { + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + if (!$phpDocInfo instanceof PhpDocInfo) { + return null; + } + + $tagsByName = $phpDocInfo->getTagsByName($configuration->getAnnotation()); + if ($tagsByName === []) { + return null; + } + + $hasAttribute = false; + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toString() === $configuration->getAttributeClass()) { + $hasAttribute = true; + break 2; + } + } + } + + $docBlockHasChanged = \false; + foreach ($tagsByName as $valueNode) { + if (!$valueNode->value instanceof GenericTagValueNode) { + continue; + } + + if ($hasAttribute === false) { + $stringValue = $valueNode->value->value; + $stringValue = '{'.trim($stringValue, '()').'}'; + $tokenIterator = $this->tokenIteratorFactory->create($stringValue); + $data = $this->arrayParser->parseCurlyArray($tokenIterator, $node); + $attribute = $this->createAttribute($configuration->getAttributeClass(), $data); + $node->attrGroups[] = new AttributeGroup([$attribute]); + } + + if (version_compare($this->installedDrupalVersion(), $configuration->getRemoveVersion(), '>=')) { + $this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $valueNode); + $docBlockHasChanged = \true; + } + } + if ($docBlockHasChanged) { + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + + return $node; + } + + return null; + } + + /** + * @param array|ArrayItemNode[] $parsedArgs + * + * @return Attribute + */ + private function createAttribute(string $attributeClass, array $parsedArgs): Attribute + { + $fullyQualified = new FullyQualified($attributeClass); + $args = []; + foreach ($parsedArgs as $value) { + if ($value->key == 'deriver') { + $arg = $this->nodeFactory->createClassConstFetch($value->value->value, 'class'); + } elseif ($value->value instanceof DoctrineAnnotationTagValueNode) { + $arg = $this->convertTranslateAnnotation($value->value); + } elseif ($value->key === 'forms') { + $attribute = $this->annotationToAttributeMapper->map($value); + $arg = $attribute->value; + } else { + $arg = new String_($value->value->value); + } + + $args[] = new Arg($arg, \false, \false, [], new Node\Identifier($value->key)); + } + + return new Attribute($fullyQualified, $args); + } + + public function convertTranslateAnnotation(DoctrineAnnotationTagValueNode $value): ?Node\Expr\New_ + { + // Check the annotation type, this will be helpful later. + if ($value->identifierTypeNode->name !== '@Translation') { + return null; + } + + $valueArg = null; + $argumentArg = null; + $contextArg = null; + + // Loop through the values of the annotation, just to make 100% sure we have the correct argument order + foreach ($value->values as $translateValue) { + if ($translateValue->key === null) { + $valueArg = $this->nodeFactory->createArg($translateValue->value->value); + } + if ($translateValue->key === 'context') { + $contextArg = $this->nodeFactory->createArg(['context' => $translateValue->value->value]); + } + if ($translateValue->key === 'arguments') { + $argumentArg = []; + foreach ($translateValue->value->values as $argumentValue) { + $argumentArg[$argumentValue->key->value] = $argumentValue->value->value; + } + $argumentArg = $this->nodeFactory->createArg($argumentArg); + } + } + + $argArray = []; + if ($valueArg !== null) { + $argArray[] = $valueArg; + } + if ($argumentArg !== null) { + $argArray[] = $argumentArg; + } + if ($contextArg !== null) { + $argArray[] = $contextArg; + } + + return new Node\Expr\New_(new Node\Name('Drupal\Core\StringTranslation\TranslatableMarkup'), $argArray); + } +} diff --git a/src/Drupal10/Rector/ValueObject/AnnotationToAttributeConfiguration.php b/src/Drupal10/Rector/ValueObject/AnnotationToAttributeConfiguration.php new file mode 100644 index 00000000..7d8ee0e5 --- /dev/null +++ b/src/Drupal10/Rector/ValueObject/AnnotationToAttributeConfiguration.php @@ -0,0 +1,46 @@ +introducedVersion = $introducedVersion; + $this->removeVersion = $removeVersion; + $this->annotation = $annotation; + $this->attributeClass = $attributeClass; + } + + public function getIntroducedVersion(): string + { + return $this->introducedVersion; + } + + public function getRemoveVersion(): string + { + return $this->removeVersion; + } + + public function getAnnotation(): string + { + return $this->annotation; + } + + public function getAttributeClass(): string + { + return $this->attributeClass; + } +} diff --git a/src/Rector/AbstractDrupalCoreRector.php b/src/Rector/AbstractDrupalCoreRector.php index 55bf1777..89ac7c5b 100644 --- a/src/Rector/AbstractDrupalCoreRector.php +++ b/src/Rector/AbstractDrupalCoreRector.php @@ -58,10 +58,8 @@ protected function isInBackwardsCompatibleCall(Node $node): bool public function refactor(Node $node) { - $drupalVersion = str_replace(['.x-dev', '-dev'], '.0', \Drupal::VERSION); - foreach ($this->configuration as $configuration) { - if (version_compare($drupalVersion, $configuration->getIntroducedVersion(), '<')) { + if ($this->rectorShouldApplyToDrupalVersion($configuration) === false) { continue; } @@ -82,7 +80,7 @@ public function refactor(Node $node) // The reason for this is that will start supplying patches for // Drupal 10 when 10.0 is already out of support. This means that // we will not support running drupal-rector on Drupal 10.0.x. - if (version_compare($drupalVersion, '10.1.0', '<') || version_compare($configuration->getIntroducedVersion(), '10.0.0', '<')) { + if ($this->supportBackwardsCompatibility($configuration) === false) { return $result; } @@ -115,4 +113,42 @@ private function createBcCallOnCallLike(Node\Expr\CallLike $node, Node\Expr\Call new ArrowFunction(['expr' => $clonedNode]), ]); } + + /** + * @param VersionedConfigurationInterface $configuration + * + * @return bool|int + */ + public function rectorShouldApplyToDrupalVersion(VersionedConfigurationInterface $configuration) + { + return version_compare($this->installedDrupalVersion(), $configuration->getIntroducedVersion(), '>='); + } + + /** + * @phpstan-return non-empty-string + */ + public function installedDrupalVersion(): string + { + return str_replace([ + '.x-dev', + '-dev', + ], '.0', \Drupal::VERSION); + } + + /** + * Check if Drupal version and the introduced version support backward + * compatible calls. Although it was introduced in Drupal 10.1 we + * also supply these patches for changes introduced in Drupal 10.0. + * The reason for this is that will start supplying patches for + * Drupal 10 when 10.0 is already out of support. This means that + * we will not support running drupal-rector on Drupal 10.0.x. + * + * @param VersionedConfigurationInterface $configuration + * + * @return bool + */ + public function supportBackwardsCompatibility(VersionedConfigurationInterface $configuration): bool + { + return !(version_compare($this->installedDrupalVersion(), '10.1.0', '<') || version_compare($configuration->getIntroducedVersion(), '10.0.0', '<')); + } } diff --git a/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/ActionAnnotationToAttributeRectorTest.php b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/ActionAnnotationToAttributeRectorTest.php new file mode 100644 index 00000000..f29f94cf --- /dev/null +++ b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/ActionAnnotationToAttributeRectorTest.php @@ -0,0 +1,35 @@ +doTestFile($filePath); + } + + /** + * @return Iterator<> + */ + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture'); + } + + public function provideConfigFilePath(): string + { + // must be implemented + return __DIR__.'/config/configured_rule.php'; + } +} diff --git a/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/BackwardsCompatibilityActionAnnotationToAttributeRectorTest.php b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/BackwardsCompatibilityActionAnnotationToAttributeRectorTest.php new file mode 100644 index 00000000..194f05a8 --- /dev/null +++ b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/BackwardsCompatibilityActionAnnotationToAttributeRectorTest.php @@ -0,0 +1,40 @@ +doTestFile($filePath); + } + + /** + * @return Iterator<> + */ + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture-next-major'); + } + + public function provideConfigFilePath(): string + { + // must be implemented + return __DIR__.'/config/configured_rule_simulate_next_major.php'; + } +} diff --git a/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/config/configured_rule.php b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/config/configured_rule.php new file mode 100644 index 00000000..b7d8687c --- /dev/null +++ b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/config/configured_rule.php @@ -0,0 +1,15 @@ + + +----- + diff --git a/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture-next-major/action_bc_existing_attribute_fixture.php.inc b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture-next-major/action_bc_existing_attribute_fixture.php.inc new file mode 100644 index 00000000..8b197dfe --- /dev/null +++ b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture-next-major/action_bc_existing_attribute_fixture.php.inc @@ -0,0 +1,28 @@ + + +----- + diff --git a/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture-next-major/action_bc_multiple_translation_arguments.php.inc b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture-next-major/action_bc_multiple_translation_arguments.php.inc new file mode 100644 index 00000000..8b1ef848 --- /dev/null +++ b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture-next-major/action_bc_multiple_translation_arguments.php.inc @@ -0,0 +1,27 @@ + + +----- + 'Argument'], ['context' => 'Validation']), type: 'system')] +class BasicExample extends ActionBase implements ContainerFactoryPluginInterface { + +} +?> diff --git a/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture/action_basic_fixture.php.inc b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture/action_basic_fixture.php.inc new file mode 100644 index 00000000..b2d29312 --- /dev/null +++ b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture/action_basic_fixture.php.inc @@ -0,0 +1,33 @@ + + +----- + diff --git a/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture/action_existing_attribute_fixture.php.inc b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture/action_existing_attribute_fixture.php.inc new file mode 100644 index 00000000..b61b3c69 --- /dev/null +++ b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture/action_existing_attribute_fixture.php.inc @@ -0,0 +1,34 @@ + + +----- + diff --git a/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture/action_multiple_translation_arguments.php.inc b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture/action_multiple_translation_arguments.php.inc new file mode 100644 index 00000000..fc1736ee --- /dev/null +++ b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture/action_multiple_translation_arguments.php.inc @@ -0,0 +1,35 @@ + + +----- + 'Argument'], ['context' => 'Validation']), type: 'system', deriver: \Drupal\Core\Action\Plugin\Action\Derivative\EntityPublishedActionDeriver::class)] +class BasicExample extends ActionBase implements ContainerFactoryPluginInterface { + +} +?> diff --git a/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture/block_basic_fixture.php.inc b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture/block_basic_fixture.php.inc new file mode 100644 index 00000000..14ee6b23 --- /dev/null +++ b/tests/src/Drupal10/Rector/Deprecation/ActionAnnotationToAttributeRector/fixture/block_basic_fixture.php.inc @@ -0,0 +1,37 @@ + + +----- + false])] +class BasicExample extends ActionBase implements ContainerFactoryPluginInterface { + +} +?>