Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Rule to convert action to attributes #257

Merged
merged 27 commits into from
Mar 8, 2024

Conversation

andypost
Copy link
Contributor

@andypost andypost commented Oct 29, 2023

Description

Rule to convert Action annotation to Attribute https://www.drupal.org/node/3395575

To-do https://www.drupal.org/node/3229001

To Test

using with core and examples module
vendor/bin/rector --xdebug -c rector.php -n process modules_/examples/modules/action_example

Drupal.org issue

https://www.drupal.org/project/rector/issues/3265946

@andypost
Copy link
Contributor Author

andypost commented Oct 29, 2023

1 file with changes
===================

1) modules_/examples/modules/action_example/src/Plugin/Action/BasicExample.php:1

    ---------- begin diff ----------
@@ @@

 namespace Drupal\action_example\Plugin\Action;

+use Drupal\Core\Action\Attribute\Action;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Action\ActionBase;
 use Drupal\Core\Messenger\MessengerInterface;
@@ @@

 /**
  * A basic example action that does nothing.
- *
- * @Action(
- *   id = "action_example_basic_action",
- *   label = @Translation("Action Example: A basic example action that does nothing"),
- *   type = "system"
- * )
  */
+#[Action(id: 'action_example_basic_action', label: new TranslatableMarkup('Action Example: A basic example action that does nothing'), type: 'system')]
 class BasicExample extends ActionBase implements ContainerFactoryPluginInterface {

   /**
    ----------- end diff -----------

Applied rules:
 * ActionAnnotationToAttributeRector (https://docs.phpunit.de/en/10.0/annotations.html#ticket)

@andypost andypost force-pushed the action-attributes branch 2 times, most recently from 714f6bb to 5f4a4e8 Compare October 29, 2023 02:51
@bbrala
Copy link
Collaborator

bbrala commented Oct 29, 2023

Good start ❤️, thank you for working on this.

I'd probably make it a ConfigurableRector for one where you can feed it a configuration valueobject. I'm assuming the input for this rector would always be the tagname, since that should always map 1-on-1 to the attribute (class)? (see FunctionToStaticValueObject that implemets VersionedConfigurationInterface) for an example of such an implementation.

Then it could work like a backwards compatible rule, where we pass the version it was introduced in (as FunctionToStaticRector). But it also means it needs new handling in the AbstractDrupalCoreRector since the BC call is not really a call (which can be called using DeprecationHelper), but the fact it does not remove the docblock annotatation. Thinking about that, it might need some refactoring to handle this case of it being a docblock, or we need to handle that in the rector itself in this case. But i need to be more awake to think about that one.

Random thought, what if we make the removal bases on a property of ActionAnnotationToAttributeRector which we can set with a setter? Then we can toggle it easily. By default remove the docblock, but allow $rector->setDocblockRemoval(false) to disable it? This could be an extra argument in the ValueObject also maybe that forces the removal/keep behaviour?

The end result would be:

  1. A ConfigurableRector that can handle refactoring any of the coming annotation changes. As long as we are consistent in the change we just add a new rule with the correct configuration
  2. A way to provide the change with or without keeping the original docblock (BC fixes yay).
  3. Easy way to convert core (which can be configurable locallly to force removal even if you are running it against 10.x)
  4. Profit.

When looking at the core change record i did also notice the following:

/*
* @Block(
 *   id = "test_block_instantiation",
 *   admin_label = @Translation("Display message")
 * )
 */
#[Block(
  id: "test_block_instantiation",
  admin_label: new TranslatableMarkup("Display message")
)]

This does mean there is nested tags going on? Do we know what those are? Is that a fixed set?

@andypost
Copy link
Contributor Author

There's issue about formatting as miltiline rectorphp/rector#8239

@andypost
Copy link
Contributor Author

The set keys of annotation is defined by class https://git.drupalcode.org/project/drupal/-/blob/11.x/core/lib/Drupal/Core/Block/Annotation/Block.php

Technically the annotation could be parsed, child nodes could be picked from properties' doc-blocks and mapped

  • @var \Drupal\Core\Annotation\Translation
  • @var \Drupal\Core\Annotation\ContextDefinition[]

$args = [];
foreach ($parsedArgs as $value) {
if ($value->key === 'label') {
$arg = new Node\Expr\New_(new Node\Name(TranslatableMarkup::class), [new Arg(new String_($value->value->values[0]->value->value))]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could be a helper method

@Berdir
Copy link

Berdir commented Oct 30, 2023

There's issue about formatting as miltiline rectorphp/rector#8239

I don't think that's related. That's not about generating multiline attributes, it's about parsing annotations with multiline strings.

@andypost
Copy link
Contributor Author

$arg = new Node\Expr\New_(new Node\Name(TranslatableMarkup::class), [new Arg(new String_($value->value->values[0]->value->value))]);
}
else {
$arg = new String_($value->value->value);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our internal projects we tend to use a const PLUGIN_ID = 'foo' pattern and reference these in our annotations. I've been playing around with a custom annotation to attribute rector rule based on this one and noticed it fails on these constants. The following seems to work, although I'm wondering if there is a better way to detect the use of a constant in the annotation.

$arg = new String_($value->value instanceof StringNode ? new String_($value->value->value) : new ConstFetch(new Node\Name($value->value)));

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See root comment.

@andypost andypost force-pushed the action-attributes branch 2 times, most recently from 676af5b to dcffdd9 Compare December 10, 2023 17:55
@bbrala bbrala marked this pull request as ready for review December 11, 2023 17:41
@bbrala bbrala requested a review from mglaman December 11, 2023 17:42
@bbrala
Copy link
Collaborator

bbrala commented Dec 12, 2023

What changed yesterday (AI generated):

  1. In the ActionAnnotationToAttributeRector.php file, the convertTranslateAnnotation method had its input parameter type and internal logic modified to work directly with the actual annotation instead of its container, simplifying the conversion process.
  2. Another significant modification is made in handling method annotation-to-attribute conversions. To support backward compatibility in refactors, the changes allow annotative refactorings only on Drupal version 10.1 or higher, ignoring lower versions.
  3. AbstractDrupalCoreRector.php was refactored to improve the readability of the methods, ensuring return types match the method names. It also checks if the Drupal version supports backward compatibility.
  4. The Drupal version in Drupal.php (\Drupal::VERSION) was updated to '10.2.x-dev'.

These changes seem to streamline the annotation handling in Drupal and improve compatibility with earlier versions.

Note:
I do think at some points we'd want a service to get the Drupal version. Then we can actually set the version we run against in the tests if we like, which will make a few things easier.

@andypost
Copy link
Contributor Author

andypost commented Mar 4, 2024

Added the rest of conversion https://www.drupal.org/node/3229001

config/drupal-10/drupal-10.2-deprecations.php Outdated Show resolved Hide resolved
Comment on lines +191 to +198
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]);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this do a better check that the annotation matching the ending class name?

Wouldn't this cause @foo to become #Foo attribute because its a generic string?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean here; you get to this code after checking a tag is present as configured. Then we move on to check if there is already an attribute. If that is present, then we loopt through the tagnames and if there was no attribute matching the relevant attribute class from the configuration we start building the attribute based on the class passed into the rector by the configuration.

Don't really see how you could break this, unless user error using their own config?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More so just:

$blah = explode('\', $configuration->getAttributeClass());
$className = end($blah);
if ($blah === substr($stringValue, 1)) {

Not entirely configured. Just that the tag matches the attribute's class name. I'm going to bet there is a "malformed" phpdoc somewhere with what the phpdoc parser considers generic tags

@bbrala
Copy link
Collaborator

bbrala commented Mar 8, 2024

Perhaps we could do more with this one by rector itself:

AnnotationToAttributeMapper $annotationToAttributeMapper
$attribute = $this->annotationToAttributeMapper->map($value);

This class (which allowd extending classes by interface) does smart things with values. It also does stuff like mapping consts and such.

Kinda worth exploring if we can maybe just hook into that for converting some. The code looked very interesting to structure the mapping. Those classes were made for doctrine itself.

@bbrala
Copy link
Collaborator

bbrala commented Mar 8, 2024

New conversions

BTW want to mention, these three would've been automatable with this MR without any problem. All known properties.

Copy link
Collaborator

@mglaman mglaman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The rule isn't configured and this allows using it for Drupal core improvements.

@agentrickard agentrickard merged commit 7a48c24 into palantirnet:main Mar 8, 2024
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants