Repository: rectorphp/rector-symfony
Pull request #722: [Symfony 7.3] add constant and test for 7.3 and test help to attribute #718
Author: login: JohJohan
Created: 2025-04-23T07:48:25Z | Merged: 2025-04-24T10:45:37Z

From: #718 
> Thanks for the report. Rule is in very early stages, based on couple diffs of referenced PR, so any test fixture of the right setup will help.
> 
> Can you send a PR with the test fixture you expect?


We would need:

1. Set support for symfony/console to 7.3 or at least define `AsCommand` has attribute `help`
2. Write a rector rule who will move help to the attribute (and make sure this is done before `InvokableCommandRector` as that will remove `configure` in total.


Diff:
diff --git a/config/sets/symfony/symfony73.php b/config/sets/symfony/symfony73.php
index 0d87fd607..1896a5e3e 100644
--- a/config/sets/symfony/symfony73.php
+++ b/config/sets/symfony/symfony73.php
@@ -3,9 +3,10 @@
 declare(strict_types=1);
 
 use Rector\Config\RectorConfig;
+use Rector\Symfony\Symfony73\Rector\Class_\CommandHelpToAttributeRector;
 use Rector\Symfony\Symfony73\Rector\Class_\InvokableCommandRector;
 
 // @see https://github.com/symfony/symfony/blame/7.3/UPGRADE-7.3.md
 
 return RectorConfig::configure()
-    ->withRules([InvokableCommandRector::class]);
+    ->withRules([CommandHelpToAttributeRector::class, InvokableCommandRector::class]);
diff --git a/rules-tests/Symfony61/Rector/Class_/CommandConfigureToAttributeRector/Fixture/call_all_option.php.inc b/rules-tests/Symfony61/Rector/Class_/CommandConfigureToAttributeRector/Fixture/call_all_option.php.inc
new file mode 100644
index 000000000..8a8360c27
--- /dev/null
+++ b/rules-tests/Symfony61/Rector/Class_/CommandConfigureToAttributeRector/Fixture/call_all_option.php.inc
@@ -0,0 +1,43 @@
+<?php
+
+namespace Rector\Symfony\Tests\Symfony61\Rector\Class_\CommandConfigureToAttributeRector\Fixture;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+final class CallAllOptions extends \Symfony\Component\Console\Command\Command
+{
+    public function configure()
+    {
+        $this->setName('sunshine')
+            ->setDescription('Some description')
+            ->setHidden(false)
+            ->setHelp('Some help text')
+            ->addArgument('argument name')
+            ->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description')
+            ->setAliases(['first', 'second']);
+    }
+}
+
+?>
+-----
+<?php
+
+namespace Rector\Symfony\Tests\Symfony61\Rector\Class_\CommandConfigureToAttributeRector\Fixture;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[\Symfony\Component\Console\Attribute\AsCommand(name: 'sunshine', description: 'Some description', aliases: ['first', 'second'], hidden: false)]
+final class CallAllOptions extends \Symfony\Component\Console\Command\Command
+{
+    public function configure()
+    {
+        $this
+            ->setHelp('Some help text')
+            ->addArgument('argument name')
+            ->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
+    }
+}
+
+?>
diff --git a/rules-tests/Symfony61/Rector/Class_/CommandConfigureToAttributeRector/Fixture/other-calls-middle.php.inc b/rules-tests/Symfony61/Rector/Class_/CommandConfigureToAttributeRector/Fixture/other-calls-middle.php.inc
deleted file mode 100644
index f5bad67f7..000000000
--- a/rules-tests/Symfony61/Rector/Class_/CommandConfigureToAttributeRector/Fixture/other-calls-middle.php.inc
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-namespace Rector\Symfony\Tests\Symfony61\Rector\Class_\CommandConfigureToAttributeRector\Fixture;
-
-final class OtherCallsMiddle extends \Symfony\Component\Console\Command\Command
-{
-    public function configure()
-    {
-        $this->setName('sunshine')
-            ->addArgument('argument name')
-            ->setAliases(['first', 'second']);
-    }
-}
-
-?>
------
-<?php
-
-namespace Rector\Symfony\Tests\Symfony61\Rector\Class_\CommandConfigureToAttributeRector\Fixture;
-
-#[\Symfony\Component\Console\Attribute\AsCommand(name: 'sunshine', aliases: ['first', 'second'])]
-final class OtherCallsMiddle extends \Symfony\Component\Console\Command\Command
-{
-    public function configure()
-    {
-        $this
-            ->addArgument('argument name');
-    }
-}
-
-?>
diff --git a/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/CommandHelpToAttributeRectorTest.php b/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/CommandHelpToAttributeRectorTest.php
new file mode 100644
index 000000000..415f405fb
--- /dev/null
+++ b/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/CommandHelpToAttributeRectorTest.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector;
+
+use Iterator;
+use PHPUnit\Framework\Attributes\DataProvider;
+use Rector\Testing\PHPUnit\AbstractRectorTestCase;
+
+final class CommandHelpToAttributeRectorTest extends AbstractRectorTestCase
+{
+    #[DataProvider('provideData')]
+    public function test(string $filePath): void
+    {
+        $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/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/Fixture/add_help_to_attribute_command.php.inc b/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/Fixture/add_help_to_attribute_command.php.inc
new file mode 100644
index 000000000..f84d14219
--- /dev/null
+++ b/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/Fixture/add_help_to_attribute_command.php.inc
@@ -0,0 +1,36 @@
+<?php
+
+namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'some_name')]
+final class AddHelpToAttributeCommand extends Command
+{
+    public function configure()
+    {
+        $this->setHelp('Some help text');
+    }
+}
+
+?>
+-----
+<?php
+
+namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'some_name', help: <<<'TXT'
+Some help text
+TXT)]
+final class AddHelpToAttributeCommand extends Command
+{
+    public function configure()
+    {
+    }
+}
+
+?>
diff --git a/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/Fixture/all_attribute_set_also_add_help.php.inc b/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/Fixture/all_attribute_set_also_add_help.php.inc
new file mode 100644
index 000000000..46fce7e7c
--- /dev/null
+++ b/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/Fixture/all_attribute_set_also_add_help.php.inc
@@ -0,0 +1,42 @@
+<?php
+
+namespace Rector\Symfony\Tests\Symfony61\Rector\Class_\CommandConfigureToAttributeRector\Fixture;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[\Symfony\Component\Console\Attribute\AsCommand(name: 'sunshine', description: 'Some description', aliases: ['first', 'second'], hidden: true)]
+final class OtherCallsMiddle extends \Symfony\Component\Console\Command\Command
+{
+    public function configure()
+    {
+        $this
+            ->setHelp('Some help text')
+            ->addArgument('argument name')
+            ->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
+    }
+}
+
+?>
+-----
+<?php
+
+namespace Rector\Symfony\Tests\Symfony61\Rector\Class_\CommandConfigureToAttributeRector\Fixture;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[\Symfony\Component\Console\Attribute\AsCommand(name: 'sunshine', description: 'Some description', aliases: ['first', 'second'], hidden: true, help: <<<'TXT'
+Some help text
+TXT)]
+final class OtherCallsMiddle extends \Symfony\Component\Console\Command\Command
+{
+    public function configure()
+    {
+        $this
+            ->addArgument('argument name')
+            ->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
+    }
+}
+
+?>
diff --git a/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/Fixture/skip_has_help_attribute_command.php.inc b/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/Fixture/skip_has_help_attribute_command.php.inc
new file mode 100644
index 000000000..e559f801c
--- /dev/null
+++ b/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/Fixture/skip_has_help_attribute_command.php.inc
@@ -0,0 +1,32 @@
+<?php
+
+namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandRector\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name' , help: 'some_help')]
+final class SkipHasHelpAttributeCommand extends Command
+{
+    public function configure()
+    {
+        $this->addArgument('argument', InputArgument::REQUIRED, 'Argument description');
+        $this->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $someArgument = $input->getArgument('argument');
+        $someOption = $input->getOption('option');
+
+        // ...
+
+        return 1;
+    }
+}
+
+?>
diff --git a/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/Fixture/skip_no_as_command_attribute_and_no_help_command.php.inc b/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/Fixture/skip_no_as_command_attribute_and_no_help_command.php.inc
new file mode 100644
index 000000000..2be9b2e76
--- /dev/null
+++ b/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/Fixture/skip_no_as_command_attribute_and_no_help_command.php.inc
@@ -0,0 +1,16 @@
+<?php
+
+namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+final class SkipNoAsCommandAttributeAndNoHelpAttributeCommand extends Command
+{
+    public function configure()
+    {
+        $this->setName('Some command');
+    }
+}
+
+?>
diff --git a/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/config/configured_rule.php b/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/config/configured_rule.php
new file mode 100644
index 000000000..e5cd37d47
--- /dev/null
+++ b/rules-tests/Symfony73/Rector/Class_/CommandHelpToAttributeRector/config/configured_rule.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+use Rector\Config\RectorConfig;
+use Rector\Symfony\Symfony73\Rector\Class_\CommandHelpToAttributeRector;
+
+return static function (RectorConfig $rectorConfig): void {
+    $rectorConfig->rule(CommandHelpToAttributeRector::class);
+};
diff --git a/rules-tests/Symfony73/Rector/Class_/InvokableCommandRector/Fixture/some_command_with_method_chaining.php.inc b/rules-tests/Symfony73/Rector/Class_/InvokableCommandRector/Fixture/some_command_with_method_chaining.php.inc
new file mode 100644
index 000000000..5c6f04c7e
--- /dev/null
+++ b/rules-tests/Symfony73/Rector/Class_/InvokableCommandRector/Fixture/some_command_with_method_chaining.php.inc
@@ -0,0 +1,62 @@
+<?php
+
+namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandRector\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name')]
+final class SomeCommandWithMethodChaining extends Command
+{
+    public function configure()
+    {
+        $this
+            ->addArgument('argument', InputArgument::REQUIRED, 'Argument description')
+            ->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $someArgument = $input->getArgument('argument');
+        $someOption = $input->getOption('option');
+
+        // ...
+
+        return 1;
+    }
+}
+
+?>
+-----
+<?php
+
+namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandRector\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name')]
+final class SomeCommandWithMethodChaining
+{
+    public function __invoke(#[\Symfony\Component\Console\Attribute\Command\Argument]
+    string $argument, #[\Symfony\Component\Console\Attribute\Command\Option]
+    $option): int
+    {
+        $someArgument = $argument;
+        $someOption = $option;
+
+        // ...
+
+        return 1;
+    }
+}
+
+?>
diff --git a/rules-tests/Symfony73/Rector/Class_/InvokableCommandRector/Fixture/some_command_with_set_help.php.inc b/rules-tests/Symfony73/Rector/Class_/InvokableCommandRector/Fixture/some_command_with_set_help.php.inc
new file mode 100644
index 000000000..354902264
--- /dev/null
+++ b/rules-tests/Symfony73/Rector/Class_/InvokableCommandRector/Fixture/some_command_with_set_help.php.inc
@@ -0,0 +1,67 @@
+<?php
+
+namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandRector\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name')]
+final class SomeCommandWithSetHelp extends Command
+{
+    public function configure()
+    {
+        $this->setHelp('argument');
+        $this->addArgument('argument', InputArgument::REQUIRED, 'Argument description');
+        $this->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $someArgument = $input->getArgument('argument');
+        $someOption = $input->getOption('option');
+
+        // ...
+
+        return 1;
+    }
+}
+
+?>
+-----
+<?php
+
+namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandRector\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name')]
+final class SomeCommandWithSetHelp
+{
+    public function configure()
+    {
+        $this->setHelp('argument');
+    }
+
+    public function __invoke(#[\Symfony\Component\Console\Attribute\Command\Argument]
+    string $argument, #[\Symfony\Component\Console\Attribute\Command\Option]
+    $option): int
+    {
+        $someArgument = $argument;
+        $someOption = $option;
+
+        // ...
+
+        return 1;
+    }
+}
+
+?>
diff --git a/rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php b/rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
new file mode 100644
index 000000000..d9a676181
--- /dev/null
+++ b/rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
@@ -0,0 +1,220 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PhpParser\Node\Stmt\Function_;
+use PhpParser\NodeVisitor;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\NodeTypeResolver\Node\AttributeKey;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: <<<'TXT'
+Some help text
+TXT)]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->getAsCommandAttribute($node);
+        if ($asCommandAttribute === null) {
+            return null;
+        }
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return null;
+            }
+        }
+
+        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
+        if (! $configureClassMethod instanceof ClassMethod) {
+            return null;
+        }
+
+        $helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
+        if (! $helpExpr instanceof String_) {
+            return null;
+        }
+
+        $wrappedHelp = new String_(
+            $helpExpr->value,
+            [
+                Attributekey::KIND => String_::KIND_NOWDOC,
+                AttributeKey::DOC_LABEL => 'TXT',
+            ]
+        );
+
+        $asCommandAttribute->args[] = new Arg($wrappedHelp, false, false, [], new Identifier('help'));
+
+        if ($configureClassMethod->stmts === []) {
+            unset($configureClassMethod);
+        }
+
+        return $node;
+    }
+
+    private function getAsCommandAttribute(Class_ $class): ?Attribute
+    {
+        foreach ($class->attrGroups as $attrGroup) {
+            foreach ($attrGroup->attrs as $attribute) {
+                if ($this->nodeNameResolver->isName($attribute->name, SymfonyAttribute::AS_COMMAND)) {
+                    return $attribute;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the argument passed to setHelp() and removes the MethodCall node.
+     */
+    private function findAndRemoveSetHelpExpr(ClassMethod $configureMethod): ?String_
+    {
+        $helpString = null;
+
+        $this->traverseNodesWithCallable(
+            (array) $configureMethod->stmts,
+            function (Node $node) use (&$helpString) {
+                if ($node instanceof Class_ || $node instanceof Function_) {
+                    return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
+                }
+                if (! $node instanceof MethodCall) {
+                    return null;
+                }
+
+                if (! $this->isName($node->name, 'setHelp')) {
+                    return null;
+                }
+
+                if ($node->isFirstClassCallable() || ! isset($node->getArgs()[0])) {
+                    return null;
+                }
+
+                $argExpr = $node->getArgs()[0]
+                    ->value;
+                if ($argExpr instanceof String_) {
+                    $helpString = $argExpr;
+                }
+
+                $parent = $node->getAttribute('parent');
+                if ($parent instanceof Expression) {
+                    unset($parent);
+                }
+
+                return $node->var;
+            }
+        );
+
+        foreach ((array) $configureMethod->stmts as $key => $stmt) {
+            if ($this->isExpressionVariableThis($stmt)) {
+                unset($configureMethod->stmts[$key]);
+            }
+        }
+
+        return $helpString;
+    }
+
+    private function isExpressionVariableThis(Stmt $stmt): bool
+    {
+        if (! $stmt instanceof Expression) {
+            return false;
+        }
+
+        if (! $stmt->expr instanceof Variable) {
+            return false;
+        }
+
+        return $this->isName($stmt->expr, 'this');
+    }
+}
diff --git a/rules/Symfony73/Rector/Class_/InvokableCommandRector.php b/rules/Symfony73/Rector/Class_/InvokableCommandRector.php
index a1ba97bd1..bb0041622 100644
--- a/rules/Symfony73/Rector/Class_/InvokableCommandRector.php
+++ b/rules/Symfony73/Rector/Class_/InvokableCommandRector.php
@@ -13,6 +13,7 @@
 use PhpParser\Node\Scalar\String_;
 use PhpParser\Node\Stmt\Class_;
 use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
 use Rector\Doctrine\NodeAnalyzer\AttributeFinder;
 use Rector\Exception\ShouldNotHappenException;
 use Rector\Rector\AbstractRector;
@@ -32,6 +33,8 @@
  */
 final class InvokableCommandRector extends AbstractRector
 {
+    private const MIGRATED_CONFIGURE_CALLS = ['addArgument', 'addOption'];
+
     public function __construct(
         private readonly AttributeFinder $attributeFinder,
         private readonly CommandArgumentsAndOptionsResolver $commandArgumentsAndOptionsResolver,
@@ -188,7 +191,32 @@ private function removeConfigureClassMethod(Class_ $class): void
                 continue;
             }
 
-            unset($class->stmts[$key]);
+            foreach ((array) $stmt->stmts as $innerKey => $innerStmt) {
+                if (! $innerStmt instanceof Expression) {
+                    continue;
+                }
+
+                $expr = $innerStmt->expr;
+                if (! $expr instanceof MethodCall) {
+                    continue;
+                }
+
+                if ($this->isFluentArgumentOptionChain($expr)) {
+                    unset($stmt->stmts[$innerKey]);
+                    continue;
+                }
+
+                if ($this->isName($expr->var, 'this')
+                    && $this->isNames($expr->name, self::MIGRATED_CONFIGURE_CALLS)) {
+                    unset($stmt->stmts[$innerKey]);
+                }
+            }
+
+            // 2. if configure() has become empty → remove the method itself
+            if ($stmt->stmts === [] || $stmt->stmts === null) {
+                unset($class->stmts[$key]);
+            }
+
             return;
         }
     }
@@ -219,4 +247,21 @@ private function replaceInputArgumentOptionFetchWithVariables(ClassMethod $execu
             return new Variable($firstArgValue->value);
         });
     }
+
+    private function isFluentArgumentOptionChain(MethodCall $call): bool
+    {
+        $current = $call;
+
+        while ($current instanceof MethodCall) {
+            // every link must be addArgument() or addOption()
+            if (! $this->isNames($current->name, self::MIGRATED_CONFIGURE_CALLS)) {
+                return false;
+            }
+
+            $current = $current->var;   // go one step left
+        }
+
+        // the left-most var must be $this
+        return $current instanceof Variable && $this->isName($current, 'this');
+    }
 }
diff --git a/src/Set/SymfonySetList.php b/src/Set/SymfonySetList.php
index c62aa0e92..7d8017fe2 100644
--- a/src/Set/SymfonySetList.php
+++ b/src/Set/SymfonySetList.php
@@ -160,6 +160,11 @@ final class SymfonySetList
      */
     final public const SYMFONY_72 = __DIR__ . '/../../config/sets/symfony/symfony72.php';
 
+    /**
+     * @var string
+     */
+    final public const SYMFONY_73 = __DIR__ . '/../../config/sets/symfony/symfony73.php';
+
     /**
      * @var string
      */
diff --git a/stubs/Symfony/Component/console/attribute/AsCommand.php b/stubs/Symfony/Component/console/attribute/AsCommand.php
new file mode 100644
index 000000000..254428940
--- /dev/null
+++ b/stubs/Symfony/Component/console/attribute/AsCommand.php
@@ -0,0 +1,53 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Attribute;
+
+if (class_exists('Symfony\Component\Console\Attribute\AsCommand')) {
+    return;
+}
+
+/**
+ * Service tag to autoconfigure commands.
+ *
+ * @final since Symfony 7.3
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+class AsCommand
+{
+    /**
+     * @param string      $name        The name of the command, used when calling it (i.e. "cache:clear")
+     * @param string|null $description The description of the command, displayed with the help page
+     * @param string[]    $aliases     The list of aliases of the command. The command will be executed when using one of them (i.e. "cache:clean")
+     * @param bool        $hidden      If true, the command won't be shown when listing all the available commands, but it can still be run as any other command
+     * @param string|null $help        The help content of the command, displayed with the help page
+     */
+    public function __construct(
+        public string $name,
+        public ?string $description = null,
+        array $aliases = [],
+        bool $hidden = false,
+        public ?string $help = null,
+    ) {
+        if (!$hidden && !$aliases) {
+            return;
+        }
+
+        $name = explode('|', $name);
+        $name = array_merge($name, $aliases);
+
+        if ($hidden && '' !== $name[0]) {
+            array_unshift($name, '');
+        }
+
+        $this->name = implode('|', $name);
+    }
+}
diff --git a/tests/Set/Symfony73/Fixture/command_remove_config.php.inc b/tests/Set/Symfony73/Fixture/command_remove_config.php.inc
new file mode 100644
index 000000000..e46999d5b
--- /dev/null
+++ b/tests/Set/Symfony73/Fixture/command_remove_config.php.inc
@@ -0,0 +1,72 @@
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name')]
+final class SomeCommand extends Command
+{
+    public function configure()
+    {
+        $this->setHelp(
+            'The <info>%command.name%</info> command will do some
+        argument passed to it:
+
+        <info>php %command.full_name% argument</info>'
+        );
+        $this->addArgument('argument', InputArgument::REQUIRED, 'Argument description');
+        $this->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $someArgument = $input->getArgument('argument');
+        $someOption = $input->getOption('option');
+
+        // ...
+
+        return 1;
+    }
+}
+
+?>
+-----
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name', help: <<<'TXT'
+The <info>%command.name%</info> command will do some
+        argument passed to it:
+
+        <info>php %command.full_name% argument</info>
+TXT)]
+final class SomeCommand
+{
+    public function __invoke(#[\Symfony\Component\Console\Attribute\Command\Argument]
+    string $argument, #[\Symfony\Component\Console\Attribute\Command\Option]
+    $option): int
+    {
+        $someArgument = $argument;
+        $someOption = $option;
+
+        // ...
+
+        return 1;
+    }
+}
+
+?>
diff --git a/tests/Set/Symfony73/Symfony73Test.php b/tests/Set/Symfony73/Symfony73Test.php
new file mode 100644
index 000000000..a4da73922
--- /dev/null
+++ b/tests/Set/Symfony73/Symfony73Test.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Tests\Set\Symfony73;
+
+use Iterator;
+use PHPUnit\Framework\Attributes\DataProvider;
+use Rector\Testing\PHPUnit\AbstractRectorTestCase;
+
+final class Symfony73Test extends AbstractRectorTestCase
+{
+    #[DataProvider('provideData')]
+    public function test(string $filePath): void
+    {
+        $this->doTestFile($filePath);
+    }
+
+    public static function provideData(): Iterator
+    {
+        return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
+    }
+
+    public function provideConfigFilePath(): string
+    {
+        return __DIR__ . '/config/symfony73.php';
+    }
+}
diff --git a/tests/Set/Symfony73/config/symfony73.php b/tests/Set/Symfony73/config/symfony73.php
new file mode 100644
index 000000000..219b3986e
--- /dev/null
+++ b/tests/Set/Symfony73/config/symfony73.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+use Rector\Config\RectorConfig;
+use Rector\Symfony\Set\SymfonySetList;
+
+return static function (RectorConfig $rectorConfig): void {
+    $rectorConfig->sets([SymfonySetList::SYMFONY_73]);
+};


Reviews:
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T12:01:34Z
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T12:03:07Z
user: login: TomasVotruba
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T12:24:21Z
user: login: TomasVotruba
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T12:26:15Z
user: login: TomasVotruba
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T12:27:17Z
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T13:35:25Z
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T13:35:27Z
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T13:35:28Z
user: login: samsonasik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T14:03:05Z
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T14:11:09Z
user: login: samsonasik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T14:19:36Z
user: login: samsonasik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T14:20:16Z
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T14:26:46Z
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T14:26:50Z
user: login: samsonasik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T14:30:24Z
user: login: samsonasik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T14:30:57Z
user: login: samsonasik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T14:38:33Z
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T08:51:58Z
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T08:54:16Z
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T08:54:29Z
user: login: samsonasik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T08:59:10Z
user: login: samsonasik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T09:02:02Z
user: login: samsonasik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T09:03:48Z
user: login: samsonasik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T09:08:44Z
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T10:24:42Z
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T10:24:48Z
user: login: JohJohan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T10:24:52Z

Review comments:
diff_hunk: @@ -0,0 +1,49 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Attribute;
+
+/**
+ * Service tag to autoconfigure commands.
+ *
+ * @final since Symfony 7.3
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+class AsCommand
path: stubs/Symfony/Component/console/attribute/AsCommand.php
user: login: JohJohan
body: This is a copy paste from 7.3 branch is that okay to do it like this?
created_at: 2025-04-23T12:01:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,6 +46,11 @@ use Symfony\Component\Console\Input\InputOption;
 #[AsCommand(name: 'some_name')]
 final class SomeCommand
 {
+    public function configure()
path: rules-tests/Symfony73/Rector/Class_/InvokableCommandRector/Fixture/some_command.php.inc
user: login: JohJohan
body: Should `configure` come after `__invoke` and should rector handle that?
created_at: 2025-04-23T12:03:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,6 +46,11 @@ use Symfony\Component\Console\Input\InputOption;
 #[AsCommand(name: 'some_name')]
 final class SomeCommand
 {
+    public function configure()
path: rules-tests/Symfony73/Rector/Class_/InvokableCommandRector/Fixture/some_command.php.inc
user: login: TomasVotruba
body: I think current state is better.
created_at: 2025-04-23T12:24:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,49 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Attribute;
+
+/**
+ * Service tag to autoconfigure commands.
+ *
+ * @final since Symfony 7.3
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+class AsCommand
path: stubs/Symfony/Component/console/attribute/AsCommand.php
user: login: TomasVotruba
body: Yes, that's fine :+1: 

Even better is to add `class_exists(...)` check above. See other stub files.

That way, if we use a /vendor/symfony/console with `AsCommand` and also have `AsCommand`, we avoid conflicting files.
created_at: 2025-04-23T12:26:15Z
author_association: MEMBER
diff_hunk: @@ -14,6 +14,7 @@ final class SomeCommand extends Command
 {
     public function configure()
     {
+        $this->setHelp('argument');
path: rules-tests/Symfony73/Rector/Class_/InvokableCommandRector/Fixture/some_command.php.inc
user: login: TomasVotruba
body: Could you add a new test fixture file only with `setHelp()` issue? 
That way we keep test fixtures separated by case. This makes them easier to maintain/debug later on
created_at: 2025-04-23T12:27:17Z
author_association: MEMBER
diff_hunk: @@ -14,6 +14,7 @@ final class SomeCommand extends Command
 {
     public function configure()
     {
+        $this->setHelp('argument');
path: rules-tests/Symfony73/Rector/Class_/InvokableCommandRector/Fixture/some_command.php.inc
user: login: JohJohan
body: I added `some_command_with_set_help.php.inc` where its the same as here but then explicitly name this test as we need to have `$this->addArgumen()` and   `$this->addOption()` for the `InvokableCommandRector` to remove the `configure` bug like before
created_at: 2025-04-23T13:35:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,6 +46,11 @@ use Symfony\Component\Console\Input\InputOption;
 #[AsCommand(name: 'some_name')]
 final class SomeCommand
 {
+    public function configure()
path: rules-tests/Symfony73/Rector/Class_/InvokableCommandRector/Fixture/some_command.php.inc
user: login: JohJohan
body: Okay then we can resolve this
created_at: 2025-04-23T13:35:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Attribute;
+
+/**
+ * Service tag to autoconfigure commands.
+ *
+ * @final since Symfony 7.3
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+class AsCommand
path: stubs/Symfony/Component/console/attribute/AsCommand.php
user: login: JohJohan
body: Added the class_exists check
created_at: 2025-04-23T13:35:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,207 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: '<<<TXT Some help text TXT')]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->ensureAsCommandAttribute($node);
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return $node;
+            }
+        }
+
+        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
+        if (! $configureClassMethod instanceof ClassMethod) {
+            return null;
+        }
+
+        $helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
+        if (! $helpExpr instanceof String_) {
+            return null;
+        }
+
+        $wrappedHelp = new String_(
+            '<<<TXT ' . $helpExpr->value . ' TXT'
path: rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
user: login: samsonasik
body: imo better use new line to ensure more support language syntax, should be something like;

```suggestion
            '<<<TXT\n' . $helpExpr->value . '\nTXT'
```
created_at: 2025-04-23T14:03:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,207 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: '<<<TXT Some help text TXT')]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->ensureAsCommandAttribute($node);
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return $node;
+            }
+        }
+
+        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
+        if (! $configureClassMethod instanceof ClassMethod) {
+            return null;
+        }
+
+        $helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
+        if (! $helpExpr instanceof String_) {
+            return null;
+        }
+
+        $wrappedHelp = new String_(
+            '<<<TXT ' . $helpExpr->value . ' TXT'
path: rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
user: login: JohJohan
body: Good point, i see Symfony also has newlines in there examples i added this and updated expected tests
created_at: 2025-04-23T14:11:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name')]
+final class SomeCommand extends Command
+{
+    public function configure()
+    {
+        $this->setHelp('The <info>%command.name%</info> command will do some
+        argument passed to it:
+
+        <info>php %command.full_name% argument</info>');
+        $this->addArgument('argument', InputArgument::REQUIRED, 'Argument description');
+        $this->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $someArgument = $input->getArgument('argument');
+        $someOption = $input->getOption('option');
+
+        // ...
+
+        return 1;
+    }
+}
+
+?>
+-----
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name', help: '<<<TXT\nThe <info>%command.name%</info> command will do some
path: tests/Set/Symfony73/Fixture/command_remove_config.php.inc
user: login: samsonasik
body: hm.., `\n` should be converted to new line, so the code maybe need in double quote so it applied as new line
created_at: 2025-04-23T14:19:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,207 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: '<<<TXT Some help text TXT')]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->ensureAsCommandAttribute($node);
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return $node;
+            }
+        }
+
+        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
+        if (! $configureClassMethod instanceof ClassMethod) {
+            return null;
+        }
+
+        $helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
+        if (! $helpExpr instanceof String_) {
+            return null;
+        }
+
+        $wrappedHelp = new String_(
+            '<<<TXT ' . $helpExpr->value . ' TXT'
path: rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
user: login: samsonasik
body: hm.., `\n` should be converted to new line, so the code maybe need in double quote so it applied as new line

created_at: 2025-04-23T14:20:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,207 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: '<<<TXT Some help text TXT')]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->ensureAsCommandAttribute($node);
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return $node;
+            }
+        }
+
+        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
+        if (! $configureClassMethod instanceof ClassMethod) {
+            return null;
+        }
+
+        $helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
+        if (! $helpExpr instanceof String_) {
+            return null;
+        }
+
+        $wrappedHelp = new String_(
+            '<<<TXT ' . $helpExpr->value . ' TXT'
path: rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
user: login: JohJohan
body: Your right double quote fixed that! I updated the tests again also updated the `CODE_SAMPLE`
created_at: 2025-04-23T14:26:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name')]
+final class SomeCommand extends Command
+{
+    public function configure()
+    {
+        $this->setHelp('The <info>%command.name%</info> command will do some
+        argument passed to it:
+
+        <info>php %command.full_name% argument</info>');
+        $this->addArgument('argument', InputArgument::REQUIRED, 'Argument description');
+        $this->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $someArgument = $input->getArgument('argument');
+        $someOption = $input->getOption('option');
+
+        // ...
+
+        return 1;
+    }
+}
+
+?>
+-----
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name', help: '<<<TXT\nThe <info>%command.name%</info> command will do some
path: tests/Set/Symfony73/Fixture/command_remove_config.php.inc
user: login: JohJohan
body: Your right double quote fixed that! I updated the tests again also updated the `CODE_SAMPLE`
created_at: 2025-04-23T14:26:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,70 @@
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name')]
+final class SomeCommand extends Command
+{
+    public function configure()
+    {
+        $this->setHelp('The <info>%command.name%</info> command will do some
+        argument passed to it:
+
+        <info>php %command.full_name% argument</info>');
+        $this->addArgument('argument', InputArgument::REQUIRED, 'Argument description');
+        $this->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $someArgument = $input->getArgument('argument');
+        $someOption = $input->getOption('option');
+
+        // ...
+
+        return 1;
+    }
+}
+
+?>
+-----
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name', help: '<<<TXT
path: tests/Set/Symfony73/Fixture/command_remove_config.php.inc
user: login: samsonasik
body: no first single quote before TXT, but inside TXT

```suggestion
#[AsCommand(name: 'some_name', help: <<<'TXT'
```
created_at: 2025-04-23T14:30:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name')]
+final class SomeCommand extends Command
+{
+    public function configure()
+    {
+        $this->setHelp('The <info>%command.name%</info> command will do some
+        argument passed to it:
+
+        <info>php %command.full_name% argument</info>');
+        $this->addArgument('argument', InputArgument::REQUIRED, 'Argument description');
+        $this->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $someArgument = $input->getArgument('argument');
+        $someOption = $input->getOption('option');
+
+        // ...
+
+        return 1;
+    }
+}
+
+?>
+-----
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name', help: '<<<TXT
+The <info>%command.name%</info> command will do some
+        argument passed to it:
+
+        <info>php %command.full_name% argument</info>
+TXT')]
path: tests/Set/Symfony73/Fixture/command_remove_config.php.inc
user: login: samsonasik
body: new line needs after TXT without "'" end quote

```suggestion
TXT
)]
```
created_at: 2025-04-23T14:30:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,209 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: '<<<TXT
+Some help text
+TXT')]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->ensureAsCommandAttribute($node);
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return $node;
+            }
+        }
+
+        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
+        if (! $configureClassMethod instanceof ClassMethod) {
+            return null;
+        }
+
+        $helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
+        if (! $helpExpr instanceof String_) {
+            return null;
+        }
+
+        $wrappedHelp = new String_(
+            "<<<TXT\n" . $helpExpr->value . "\nTXT"
path: rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
user: login: samsonasik
body: I think you need to use  attribute KIND NOWDOC,  see

https://github.com/nikic/PHP-Parser/blob/c9d0b6c9f2c2c323661d31f7c81a26bf479ae3ff/lib/PhpParser/Node/Scalar/String_.php#L13

something like;

```php
$wrappedHelp = new String_( $helpExpr->value, [Attributekey::KIND => String_::KIND_NOWDOC]) 
```
created_at: 2025-04-23T14:38:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name')]
+final class SomeCommand extends Command
+{
+    public function configure()
+    {
+        $this->setHelp('The <info>%command.name%</info> command will do some
+        argument passed to it:
+
+        <info>php %command.full_name% argument</info>');
+        $this->addArgument('argument', InputArgument::REQUIRED, 'Argument description');
+        $this->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $someArgument = $input->getArgument('argument');
+        $someOption = $input->getOption('option');
+
+        // ...
+
+        return 1;
+    }
+}
+
+?>
+-----
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name', help: '<<<TXT
path: tests/Set/Symfony73/Fixture/command_remove_config.php.inc
user: login: JohJohan
body: I updated my code to implement this and updated the tests
created_at: 2025-04-24T08:51:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,209 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: '<<<TXT
+Some help text
+TXT')]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->ensureAsCommandAttribute($node);
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return $node;
+            }
+        }
+
+        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
+        if (! $configureClassMethod instanceof ClassMethod) {
+            return null;
+        }
+
+        $helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
+        if (! $helpExpr instanceof String_) {
+            return null;
+        }
+
+        $wrappedHelp = new String_(
+            "<<<TXT\n" . $helpExpr->value . "\nTXT"
path: rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
user: login: JohJohan
body: I updated my code to build the `String()` like this but i got `TXT)]` in the end as there is no support in adding the new line after `TXT` is that okay? I think that is also a formatting thing and rector doesn't do that right
created_at: 2025-04-24T08:54:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,70 @@
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name')]
+final class SomeCommand extends Command
+{
+    public function configure()
+    {
+        $this->setHelp('The <info>%command.name%</info> command will do some
+        argument passed to it:
+
+        <info>php %command.full_name% argument</info>');
+        $this->addArgument('argument', InputArgument::REQUIRED, 'Argument description');
+        $this->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $someArgument = $input->getArgument('argument');
+        $someOption = $input->getOption('option');
+
+        // ...
+
+        return 1;
+    }
+}
+
+?>
+-----
+<?php
+
+namespace Rector\Symfony\Tests\Set\Symfony73\Fixture;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+
+#[AsCommand(name: 'some_name', help: '<<<TXT
+The <info>%command.name%</info> command will do some
+        argument passed to it:
+
+        <info>php %command.full_name% argument</info>
+TXT')]
path: tests/Set/Symfony73/Fixture/command_remove_config.php.inc
user: login: JohJohan
body: See https://github.com/rectorphp/rector-symfony/pull/722#discussion_r2057895383
created_at: 2025-04-24T08:54:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,209 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: '<<<TXT
+Some help text
+TXT')]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->ensureAsCommandAttribute($node);
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return $node;
+            }
+        }
+
+        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
+        if (! $configureClassMethod instanceof ClassMethod) {
+            return null;
+        }
+
+        $helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
+        if (! $helpExpr instanceof String_) {
+            return null;
+        }
+
+        $wrappedHelp = new String_(
+            "<<<TXT\n" . $helpExpr->value . "\nTXT"
path: rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
user: login: samsonasik
body: symfony 7.3 require php 8.2 so I think it is fine 👍 
created_at: 2025-04-24T08:59:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,214 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\NodeTypeResolver\Node\AttributeKey;
+use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: <<<'TXT'
+Some help text
+TXT)]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->ensureAsCommandAttribute($node);
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return $node;
+            }
+        }
+
+        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
+        if (! $configureClassMethod instanceof ClassMethod) {
+            return null;
+        }
+
+        $helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
+        if (! $helpExpr instanceof String_) {
+            return null;
+        }
+
+        $wrappedHelp = new String_(
+            $helpExpr->value,
+            [
+                Attributekey::KIND => String_::KIND_NOWDOC,
+                AttributeKey::DOC_LABEL => 'TXT',
+            ]
+        );
+
+        $asCommandAttribute->args[] = new Arg($wrappedHelp, false, false, [], new Identifier('help'));
+
+        if ($configureClassMethod->stmts === []) {
+            unset($configureClassMethod);
+        }
+
+        return $node;
+    }
+
+    private function ensureAsCommandAttribute(Class_ $class): Attribute
+    {
+        foreach ($class->attrGroups as $attrGroup) {
+            foreach ($attrGroup->attrs as $attribute) {
+                if ($this->nodeNameResolver->isName($attribute->name, SymfonyAttribute::AS_COMMAND)) {
+                    return $attribute;
+                }
+            }
+        }
+
+        $attrGroup = $this->phpAttributeGroupFactory->createFromClass(SymfonyAttribute::AS_COMMAND);
+        $class->attrGroups[] = $attrGroup;
+
+        return $attrGroup->attrs[0];
+    }
+
+    /**
+     * Returns the argument passed to setHelp() and removes the MethodCall node.
+     */
+    private function findAndRemoveSetHelpExpr(ClassMethod $configureMethod): ?String_
+    {
+        $helpString = null;
+
+        $this->traverseNodesWithCallable(
+            (array) $configureMethod->stmts,
+            function (Node $node) use (&$helpString): ?Expr {
+                if (! $node instanceof MethodCall) {
+                    return null;
+                }
+
+                if (! $this->isName($node->name, 'setHelp')) {
+                    return null;
+                }
+
+                $argExpr = $node->getArgs()[0]
path: rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
user: login: samsonasik
body: ensure `[0]` index exists and not `firstClassCallable()` something like this pseudo code

```php
if ($node->isFirstClassCallable() || ! isset($node->getArgs()[0])) {
   return null;
}
```
created_at: 2025-04-24T09:02:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,214 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\NodeTypeResolver\Node\AttributeKey;
+use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: <<<'TXT'
+Some help text
+TXT)]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->ensureAsCommandAttribute($node);
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return $node;
+            }
+        }
+
+        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
+        if (! $configureClassMethod instanceof ClassMethod) {
+            return null;
+        }
+
+        $helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
+        if (! $helpExpr instanceof String_) {
+            return null;
+        }
+
+        $wrappedHelp = new String_(
+            $helpExpr->value,
+            [
+                Attributekey::KIND => String_::KIND_NOWDOC,
+                AttributeKey::DOC_LABEL => 'TXT',
+            ]
+        );
+
+        $asCommandAttribute->args[] = new Arg($wrappedHelp, false, false, [], new Identifier('help'));
+
+        if ($configureClassMethod->stmts === []) {
+            unset($configureClassMethod);
+        }
+
+        return $node;
+    }
+
+    private function ensureAsCommandAttribute(Class_ $class): Attribute
+    {
+        foreach ($class->attrGroups as $attrGroup) {
+            foreach ($attrGroup->attrs as $attribute) {
+                if ($this->nodeNameResolver->isName($attribute->name, SymfonyAttribute::AS_COMMAND)) {
+                    return $attribute;
+                }
+            }
+        }
+
+        $attrGroup = $this->phpAttributeGroupFactory->createFromClass(SymfonyAttribute::AS_COMMAND);
+        $class->attrGroups[] = $attrGroup;
+
+        return $attrGroup->attrs[0];
+    }
+
+    /**
+     * Returns the argument passed to setHelp() and removes the MethodCall node.
+     */
+    private function findAndRemoveSetHelpExpr(ClassMethod $configureMethod): ?String_
+    {
+        $helpString = null;
+
+        $this->traverseNodesWithCallable(
+            (array) $configureMethod->stmts,
+            function (Node $node) use (&$helpString): ?Expr {
+                if (! $node instanceof MethodCall) {
path: rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
user: login: samsonasik
body: don't change under inner `Class_` or `Function_`:

```php
if ($node instanceof \PhpParser\Node\Stmt\Class_ || $node instanceof \PhpParser\Node\Stmt\Function_) {
    return \PhpParser\NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
}
```
created_at: 2025-04-24T09:03:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,214 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\NodeTypeResolver\Node\AttributeKey;
+use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: <<<'TXT'
+Some help text
+TXT)]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->ensureAsCommandAttribute($node);
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return $node;
path: rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
user: login: samsonasik
body: this seems can return null when no change on `ensureAsCommandAttribute` call
created_at: 2025-04-24T09:08:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,214 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\NodeTypeResolver\Node\AttributeKey;
+use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: <<<'TXT'
+Some help text
+TXT)]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->ensureAsCommandAttribute($node);
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return $node;
+            }
+        }
+
+        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
+        if (! $configureClassMethod instanceof ClassMethod) {
+            return null;
+        }
+
+        $helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
+        if (! $helpExpr instanceof String_) {
+            return null;
+        }
+
+        $wrappedHelp = new String_(
+            $helpExpr->value,
+            [
+                Attributekey::KIND => String_::KIND_NOWDOC,
+                AttributeKey::DOC_LABEL => 'TXT',
+            ]
+        );
+
+        $asCommandAttribute->args[] = new Arg($wrappedHelp, false, false, [], new Identifier('help'));
+
+        if ($configureClassMethod->stmts === []) {
+            unset($configureClassMethod);
+        }
+
+        return $node;
+    }
+
+    private function ensureAsCommandAttribute(Class_ $class): Attribute
+    {
+        foreach ($class->attrGroups as $attrGroup) {
+            foreach ($attrGroup->attrs as $attribute) {
+                if ($this->nodeNameResolver->isName($attribute->name, SymfonyAttribute::AS_COMMAND)) {
+                    return $attribute;
+                }
+            }
+        }
+
+        $attrGroup = $this->phpAttributeGroupFactory->createFromClass(SymfonyAttribute::AS_COMMAND);
+        $class->attrGroups[] = $attrGroup;
+
+        return $attrGroup->attrs[0];
+    }
+
+    /**
+     * Returns the argument passed to setHelp() and removes the MethodCall node.
+     */
+    private function findAndRemoveSetHelpExpr(ClassMethod $configureMethod): ?String_
+    {
+        $helpString = null;
+
+        $this->traverseNodesWithCallable(
+            (array) $configureMethod->stmts,
+            function (Node $node) use (&$helpString): ?Expr {
+                if (! $node instanceof MethodCall) {
path: rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
user: login: JohJohan
body: Added this if check
created_at: 2025-04-24T10:24:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,214 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\NodeTypeResolver\Node\AttributeKey;
+use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: <<<'TXT'
+Some help text
+TXT)]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->ensureAsCommandAttribute($node);
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return $node;
path: rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
user: login: JohJohan
body: I made it so `CommandHelpToAttributeRector.php` wouldn't add `AsCommand` attribute when it doesn't exists i think its not the job of the rector rule it should have been done by `CommandConfigureToAttributeRector`
created_at: 2025-04-24T10:24:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,214 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Rector\Symfony\Symfony73\Rector\Class_;
+
+use PhpParser\Node;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Attribute;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\MethodCall;
+use PhpParser\Node\Expr\Variable;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Scalar\String_;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Expression;
+use PHPStan\Reflection\ReflectionProvider;
+use PHPStan\Type\ObjectType;
+use Rector\NodeTypeResolver\Node\AttributeKey;
+use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
+use Rector\Rector\AbstractRector;
+use Rector\Symfony\Enum\CommandMethodName;
+use Rector\Symfony\Enum\SymfonyAttribute;
+use Rector\Symfony\Enum\SymfonyClass;
+use Rector\ValueObject\PhpVersionFeature;
+use Rector\VersionBonding\Contract\MinPhpVersionInterface;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @see https://symfony.com/doc/current/console.html#help-message
+ *
+ * @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
+ */
+final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
+{
+    public function __construct(
+        private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
+        private readonly ReflectionProvider $reflectionProvider,
+    ) {
+    }
+
+    public function provideMinPhpVersion(): int
+    {
+        return PhpVersionFeature::ATTRIBUTES;
+    }
+
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition(
+            'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
+            [
+                new CodeSample(
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Command\Command;
+
+final class SomeCommand extends Command
+{
+    protected function configure(): void
+    {
+        $this->setHelp('Some help text');
+    }
+}
+CODE_SAMPLE
+                    ,
+                    <<<'CODE_SAMPLE'
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+
+#[AsCommand(name: 'app:some', help: <<<'TXT'
+Some help text
+TXT)]
+final class SomeCommand extends Command
+{
+}
+CODE_SAMPLE
+                ),
+            ]
+        );
+    }
+
+    /**
+     * @return array<class-string<Node>>
+     */
+    public function getNodeTypes(): array
+    {
+        return [Class_::class];
+    }
+
+    /**
+     * @param Class_ $node
+     */
+    public function refactor(Node $node): ?Node
+    {
+        if ($node->isAbstract()) {
+            return null;
+        }
+
+        if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
+            return null;
+        }
+
+        if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
+            return null;
+        }
+
+        $asCommandAttribute = $this->ensureAsCommandAttribute($node);
+
+        foreach ($asCommandAttribute->args as $arg) {
+            if ($arg->name?->toString() === 'help') {
+                return $node;
+            }
+        }
+
+        $configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
+        if (! $configureClassMethod instanceof ClassMethod) {
+            return null;
+        }
+
+        $helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
+        if (! $helpExpr instanceof String_) {
+            return null;
+        }
+
+        $wrappedHelp = new String_(
+            $helpExpr->value,
+            [
+                Attributekey::KIND => String_::KIND_NOWDOC,
+                AttributeKey::DOC_LABEL => 'TXT',
+            ]
+        );
+
+        $asCommandAttribute->args[] = new Arg($wrappedHelp, false, false, [], new Identifier('help'));
+
+        if ($configureClassMethod->stmts === []) {
+            unset($configureClassMethod);
+        }
+
+        return $node;
+    }
+
+    private function ensureAsCommandAttribute(Class_ $class): Attribute
+    {
+        foreach ($class->attrGroups as $attrGroup) {
+            foreach ($attrGroup->attrs as $attribute) {
+                if ($this->nodeNameResolver->isName($attribute->name, SymfonyAttribute::AS_COMMAND)) {
+                    return $attribute;
+                }
+            }
+        }
+
+        $attrGroup = $this->phpAttributeGroupFactory->createFromClass(SymfonyAttribute::AS_COMMAND);
+        $class->attrGroups[] = $attrGroup;
+
+        return $attrGroup->attrs[0];
+    }
+
+    /**
+     * Returns the argument passed to setHelp() and removes the MethodCall node.
+     */
+    private function findAndRemoveSetHelpExpr(ClassMethod $configureMethod): ?String_
+    {
+        $helpString = null;
+
+        $this->traverseNodesWithCallable(
+            (array) $configureMethod->stmts,
+            function (Node $node) use (&$helpString): ?Expr {
+                if (! $node instanceof MethodCall) {
+                    return null;
+                }
+
+                if (! $this->isName($node->name, 'setHelp')) {
+                    return null;
+                }
+
+                $argExpr = $node->getArgs()[0]
path: rules/Symfony73/Rector/Class_/CommandHelpToAttributeRector.php
user: login: JohJohan
body: Added these checks
created_at: 2025-04-24T10:24:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: TomasVotruba
created_at: 2025-04-23T07:54:09Z
body: Awesome, thanks for the test fixture :+1: 

Could you add fix as well?
author_association: MEMBER
user: login: TomasVotruba
created_at: 2025-04-23T13:39:48Z
body: Looks good 👍
author_association: MEMBER
user: login: JohJohan
created_at: 2025-04-24T10:29:34Z
body: @samsonasik note i did:

1. Added fixture to call all for `CommandConfigureToAttributeRector` (to check that the output will work for `CommandHelpToAttributeRector` and `InvokableCommandRector`
2. For `InvokableCommandRector` i made sure we also cleanup method chaining calls (also a fixture for this)
3. For `CommandConfigureToAttributeRector` added a fixture that has all attributes set from example `CommandConfigureToAttributeRector` rule and will add `help` attribute
author_association: CONTRIBUTOR
user: login: samsonasik
created_at: 2025-04-24T10:45:22Z
body: Let's give it a try, thank you @JohJohan 
author_association: MEMBER
user: login: TomasVotruba
created_at: 2025-04-24T12:08:04Z
body: Thakn you @JohJohan  :clap: 
author_association: MEMBER