Skip to content

[Symfony61] Do not add #[AsCommand] without a name in CommandConfigureToAttributeRector - #1102

Merged
TomasVotruba merged 2 commits into
rectorphp:mainfrom
kix:fix/command-configure-to-attribute-require-name
Nov 14, 2026
Merged

TomasVotruba merged 2 commits into
rectorphp:mainfrom
kix:fix/command-configure-to-attribute-require-name

Conversation

@kix

@kix kix commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

CommandConfigureToAttributeRector moves setName(), setDescription(), setAliases() and setHidden() out of configure() into #[AsCommand(...)]. It never checks that a name ends up in the attribute.

AsCommand::__construct() takes a required string $name as its first argument. When a command gets its name from somewhere else, for example the constructor:

final class SomeCommand extends Command
{
    public function __construct(?string $name = 'app:some')
    {
        parent::__construct($name);
    }

    protected function configure(): void
    {
        $this->setDescription('Some description');
    }
}

the rule currently produces #[AsCommand(description: 'Some description')]. Compiling the container (or booting the console) then fails with:

ArgumentCountError: Symfony\Component\Console\Attribute\AsCommand::__construct(): Argument #1 ($name) not passed

We hit this running the Symfony 6.0-7.2 sets on a Symfony 7.4 app: two commands got a nameless attribute and every bin/console call crashed.

Change

Before extracting anything, require that a name will be there: either already on an existing #[AsCommand] (name:) or set by a constant setName() in configure(). If there is something to move into the attribute but no name, the class is left untouched (same approach as the existing skip for non-constant values). configure() bodies with nothing to move, like empty_configure_body, still behave as before.

Tests

New fixtures:

  • skip_name_passed_to_parent_constructor: the case above, left unchanged. Fails without the change.
  • skip_non_constant_name: setName($this->commandName) next to a constant description, left unchanged. Fails without the change.
  • description_with_name_on_existing_attribute: #[AsCommand(name: ...)] plus a description in configure() still converts (regression guard).

The rule's tests pass (17), plus ECS and PHPStan.

🤖 Generated with Claude Code

if (! $node instanceof MethodCall || ! $this->isName($node->name, $methodName)) {
return null;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

early check :

if ($node->isFirstClassCallable()) {
    return null;
}

here ...

@bbrala

bbrala commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

We hit the same bug running the composer-based Symfony set (withComposerBased(symfony: true), Rector 2.8.4, Symfony 8.2.0-BETA1) on Drupal core.

Drupal\Core\Command\ServerCommand passes its name to parent::__construct('server') and only calls setDescription() in configure(). The rule turned that into #[AsCommand(description: 'Starts up a webserver for a site.')], and every core/scripts/drupal call then fails with:

ArgumentCountError: Symfony\Component\Console\Attribute\AsCommand::__construct(): Argument #1 ($name) not passed

The other 11 commands in core set their name with setName() and convert fine.

Commit with the change: https://git.drupalcode.org/issue/drupal-3638148/-/commit/0283aebd5ce13193397e2c300eb07786caf91187
CI: https://git.drupalcode.org/project/drupal/-/merge_requests/17813 (all Build tests fail on this)

I ported the check from this PR onto the rule bundled in Rector 2.8.4 and re-ran it on core: ServerCommand is now skipped, and DbDumpCommand (which uses setName()) still converts to #[AsCommand(name: 'dump-database-d8-mysql', description: '...')]. So this fixes it for us. Would be great to get it in.

bbrala added a commit to palantirnet/drupal-rector that referenced this pull request Nov 14, 2026
)

It moves setDescription()/setAliases()/setHidden() into #[AsCommand(...)]
without making sure the attribute gets a name. Commands that name themselves
through the constructor, like core's ServerCommand, end up with
#[AsCommand(description: '...')] and the console fatals because AsCommand's
$name is required.

Remove once rectorphp/rector-symfony#1102 is released.

https://www.drupal.org/i/3638148
@TomasVotruba

Copy link
Copy Markdown
Member

I'll look into it...

kix and others added 2 commits November 14, 2026 13:40
…eToAttributeRector

#[AsCommand] requires `name`. When configure() only sets a description, aliases or
hidden, and the name is not on the attribute nor set by a constant setName() (it is
passed to the parent constructor, for example), the rule created
#[AsCommand(description: '...')], which throws ArgumentCountError when the container
or the console boots. Leave such classes untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@TomasVotruba
TomasVotruba force-pushed the fix/command-configure-to-attribute-require-name branch from 7adfaaf to e87a48d Compare November 14, 2026 12:41
@TomasVotruba
TomasVotruba merged commit 8926cbc into rectorphp:main Nov 14, 2026
8 checks passed
@TomasVotruba

Copy link
Copy Markdown
Member

Thank you 👍

@TomasVotruba

Copy link
Copy Markdown
Member

I'll make new release today 🤞

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants