From 2fea7a8a543d2a2d00d77acce71ef2600a6302e6 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 10 Sep 2025 11:42:09 +0200 Subject: [PATCH 1/5] keep option details --- tests/Set/Symfony73/Fixture/command_remove_config.php.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Set/Symfony73/Fixture/command_remove_config.php.inc b/tests/Set/Symfony73/Fixture/command_remove_config.php.inc index a56741266..56b0ac90f 100644 --- a/tests/Set/Symfony73/Fixture/command_remove_config.php.inc +++ b/tests/Set/Symfony73/Fixture/command_remove_config.php.inc @@ -57,7 +57,7 @@ TXT)] final class SomeCommand { public function __invoke(#[\Symfony\Component\Console\Attribute\Argument(name: 'argument', description: 'Argument description')] - string $argument, #[\Symfony\Component\Console\Attribute\Option] + string $argument, #[\Symfony\Component\Console\Attribute\Option(name: 'option', shortcut: 'o', description: 'Option description', mode: \Symfony\Component\Console\Input\InputOption::VALUE_NONE)] $option): int { $someArgument = $argument; From c5f552c8aa379f93664553b4b59bd3a8f9543af5 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 10 Sep 2025 11:46:16 +0200 Subject: [PATCH 2/5] add option name --- .../CommandArgumentsAndOptionsResolver.php | 2 +- .../NodeFactory/CommandInvokeParamsFactory.php | 8 +++++--- rules/Symfony73/ValueObject/CommandOption.php | 17 +++++++++++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/rules/Symfony73/NodeAnalyzer/CommandArgumentsAndOptionsResolver.php b/rules/Symfony73/NodeAnalyzer/CommandArgumentsAndOptionsResolver.php index c5ef8c540..1a2e6cde5 100644 --- a/rules/Symfony73/NodeAnalyzer/CommandArgumentsAndOptionsResolver.php +++ b/rules/Symfony73/NodeAnalyzer/CommandArgumentsAndOptionsResolver.php @@ -89,7 +89,7 @@ public function collectCommandOptions(ClassMethod $configureClassMethod): array $optionName = $nameArgValue->value; - $commandOptionMetadatas[] = new CommandOption($optionName); + $commandOptionMetadatas[] = new CommandOption($nameArgValue); } return $commandOptionMetadatas; diff --git a/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php b/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php index 323ebef07..aaa9478b1 100644 --- a/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php +++ b/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php @@ -86,13 +86,15 @@ private function createOptionParams(array $commandOptions): array $optionParams = []; foreach ($commandOptions as $commandOption) { - $optionName = $commandOption->getName(); + $optionName = $commandOption->getStringName(); $variableName = str_replace('-', '_', $optionName); $optionParam = new Param(new Variable($variableName)); - // @todo fill type or default value + $optionParam->attrGroups[] = new AttributeGroup([ - new Attribute(new FullyQualified(SymfonyAttribute::COMMAND_OPTION)), + new Attribute(new FullyQualified(SymfonyAttribute::COMMAND_OPTION), [ + new Arg(value: $commandOption->getName(), name: new Identifier('name')), + ]), ]); $optionParams[] = $optionParam; diff --git a/rules/Symfony73/ValueObject/CommandOption.php b/rules/Symfony73/ValueObject/CommandOption.php index b6880612b..ea6819e0a 100644 --- a/rules/Symfony73/ValueObject/CommandOption.php +++ b/rules/Symfony73/ValueObject/CommandOption.php @@ -4,17 +4,30 @@ namespace Rector\Symfony\Symfony73\ValueObject; +use PhpParser\Node\Expr; +use PhpParser\Node\Scalar\String_; +use Rector\Exception\NotImplementedYetException; + final readonly class CommandOption { public function __construct( - private string $name, + private Expr $name, // @todo type // @todo default value ) { } - public function getName(): string + public function getName(): Expr { return $this->name; } + + public function getStringName(): string + { + if ($this->name instanceof String_) { + return $this->name->value; + } + + throw new NotImplementedYetException(sprintf('Add more options to "%s"', self::class)); + } } From d037307df5462f3bf01b580f1b869bff6a2e41c4 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 10 Sep 2025 11:51:12 +0200 Subject: [PATCH 3/5] tidy up --- .../CommandArgumentsAndOptionsResolver.php | 131 ------------------ .../NodeAnalyzer/CommandArgumentsResolver.php | 87 ++++++++++++ .../NodeAnalyzer/CommandOptionsResolver.php | 42 ++++++ .../CommandInvokeParamsFactory.php | 6 +- .../Symfony73/NodeFinder/MethodCallFinder.php | 53 +++++++ .../InvokableCommandInputAttributeRector.php | 13 +- .../Symfony73/ValueObject/CommandArgument.php | 8 +- rules/Symfony73/ValueObject/CommandOption.php | 2 - 8 files changed, 195 insertions(+), 147 deletions(-) delete mode 100644 rules/Symfony73/NodeAnalyzer/CommandArgumentsAndOptionsResolver.php create mode 100644 rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php create mode 100644 rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php create mode 100644 rules/Symfony73/NodeFinder/MethodCallFinder.php diff --git a/rules/Symfony73/NodeAnalyzer/CommandArgumentsAndOptionsResolver.php b/rules/Symfony73/NodeAnalyzer/CommandArgumentsAndOptionsResolver.php deleted file mode 100644 index 1a2e6cde5..000000000 --- a/rules/Symfony73/NodeAnalyzer/CommandArgumentsAndOptionsResolver.php +++ /dev/null @@ -1,131 +0,0 @@ -findMethodCallsByName($configureClassMethod, 'addArgument'); - - $commandArguments = []; - foreach ($addArgumentMethodCalls as $addArgumentMethodCall) { - // @todo extract name, type and requirements - $addArgumentArgs = $addArgumentMethodCall->getArgs(); - - $optionName = $this->valueResolver->getValue($addArgumentArgs[0]->value); - if ($optionName === null) { - // we need string value, otherwise param will not have a name - throw new ShouldNotHappenException('Argument name is required'); - } - - $mode = isset($addArgumentArgs[1]) - ? $this->valueResolver->getValue($addArgumentArgs[1]->value) - : null; - - if ($mode !== null && ! is_numeric($mode)) { - // we need numeric value or null, otherwise param will not have a name - throw new ShouldNotHappenException('Argument mode is required to be null or numeric'); - } - - $description = isset($addArgumentArgs[2]) - ? $this->valueResolver->getValue($addArgumentArgs[2]->value) - : null; - - if (! is_string($description)) { - // we need string value, otherwise param will not have a name - throw new ShouldNotHappenException('Argument description is required'); - } - - $commandArguments[] = new CommandArgument( - $addArgumentArgs[0]->value, - $addArgumentArgs[1]->value, - $addArgumentArgs[2]->value - ); - } - - return $commandArguments; - } - - /** - * @return CommandOption[] - */ - public function collectCommandOptions(ClassMethod $configureClassMethod): array - { - $addOptionMethodCalls = $this->findMethodCallsByName($configureClassMethod, 'addOption'); - - $commandOptionMetadatas = []; - foreach ($addOptionMethodCalls as $addOptionMethodCall) { - // @todo extract name, type and requirements - $addOptionArgs = $addOptionMethodCall->getArgs(); - - $nameArgValue = $addOptionArgs[0]->value; - if (! $nameArgValue instanceof String_) { - // we need string value, otherwise param will not have a name - throw new ShouldNotHappenException('Option name is required'); - } - - $optionName = $nameArgValue->value; - - $commandOptionMetadatas[] = new CommandOption($nameArgValue); - } - - return $commandOptionMetadatas; - } - - /** - * @return MethodCall[] - */ - private function findMethodCallsByName(ClassMethod $classMethod, string $desiredMethodName): array - { - $calls = []; - - $shouldReverse = false; - $this->simpleCallableNodeTraverser->traverseNodesWithCallable( - $classMethod, - function (Node $node) use (&$calls, $desiredMethodName, &$shouldReverse): null { - if (! $node instanceof MethodCall) { - return null; - } - - if (! $node->name instanceof Identifier) { - return null; - } - - if ($node->name->toString() === $desiredMethodName) { - if ($node->var instanceof MethodCall) { - $shouldReverse = true; - } - - $calls[] = $node; - } - - return null; - } - ); - - return $shouldReverse ? array_reverse($calls) : $calls; - } -} diff --git a/rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php b/rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php new file mode 100644 index 000000000..2bd5d528f --- /dev/null +++ b/rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php @@ -0,0 +1,87 @@ +methodCallFinder->find($configureClassMethod, 'addArgument'); + + $commandArguments = []; + foreach ($addArgumentMethodCalls as $addArgumentMethodCall) { + $addArgumentArgs = $addArgumentMethodCall->getArgs(); + + $optionName = $this->valueResolver->getValue($addArgumentArgs[0]->value); + if ($optionName === null) { + // we need string value, otherwise param will not have a name + throw new ShouldNotHappenException('Argument name is required'); + } + + $argumentModeExpr = $this->resolveArgumentModeExpr($addArgumentArgs); + $argumentDescriptionExpr = $this->resolveArgumentDescriptionExpr($addArgumentArgs); + + $commandArguments[] = new CommandArgument( + $addArgumentArgs[0]->value, + $argumentModeExpr, + $argumentDescriptionExpr + ); + } + + return $commandArguments; + } + + /** + * @param Node\Arg[] $addArgumentArgs + */ + private function resolveArgumentModeExpr(array $addArgumentArgs): ?Node\Expr + { + if (! isset($addArgumentArgs[1])) { + return null; + } + + $mode = $this->valueResolver->getValue($addArgumentArgs[1]->value); + if ($mode !== null && ! is_numeric($mode)) { + // we need numeric value or null, otherwise param will not have a name + throw new ShouldNotHappenException('Argument mode is required to be null or numeric'); + } + + return $addArgumentArgs[1]->value; + } + + /** + * @param Node\Arg[] $addArgumentArgs + */ + private function resolveArgumentDescriptionExpr(array $addArgumentArgs): ?Node\Expr + { + if (! isset($addArgumentArgs[2])) { + return null; + } + + $description = $this->valueResolver->getValue($addArgumentArgs[2]->value); + if (! is_string($description)) { + // we need string value, otherwise param will not have a name + throw new ShouldNotHappenException('Argument description is required'); + } + + return $addArgumentArgs[2]->value; + } +} diff --git a/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php b/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php new file mode 100644 index 000000000..06a031114 --- /dev/null +++ b/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php @@ -0,0 +1,42 @@ +methodCallFinder->find($configureClassMethod, 'addOption'); + + $commandOptionMetadatas = []; + foreach ($addOptionMethodCalls as $addOptionMethodCall) { + $addOptionArgs = $addOptionMethodCall->getArgs(); + + $nameArgValue = $addOptionArgs[0]->value; + if (! $nameArgValue instanceof String_) { + // we need string value, otherwise param will not have a name + throw new ShouldNotHappenException('Option name is required'); + } + + $commandOptionMetadatas[] = new CommandOption($nameArgValue); + } + + return $commandOptionMetadatas; + } +} diff --git a/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php b/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php index aaa9478b1..a066cc8ef 100644 --- a/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php +++ b/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php @@ -91,10 +91,10 @@ private function createOptionParams(array $commandOptions): array $optionParam = new Param(new Variable($variableName)); + $optionArgs = [new Arg(value: $commandOption->getName(), name: new Identifier('name'))]; + $optionParam->attrGroups[] = new AttributeGroup([ - new Attribute(new FullyQualified(SymfonyAttribute::COMMAND_OPTION), [ - new Arg(value: $commandOption->getName(), name: new Identifier('name')), - ]), + new Attribute(new FullyQualified(SymfonyAttribute::COMMAND_OPTION), $optionArgs), ]); $optionParams[] = $optionParam; diff --git a/rules/Symfony73/NodeFinder/MethodCallFinder.php b/rules/Symfony73/NodeFinder/MethodCallFinder.php new file mode 100644 index 000000000..89c22c1eb --- /dev/null +++ b/rules/Symfony73/NodeFinder/MethodCallFinder.php @@ -0,0 +1,53 @@ +simpleCallableNodeTraverser->traverseNodesWithCallable( + $classMethod, + function (Node $node) use (&$calls, $desiredMethodName, &$shouldReverse): null { + if (! $node instanceof MethodCall) { + return null; + } + + if (! $node->name instanceof Identifier) { + return null; + } + + if ($node->name->toString() === $desiredMethodName) { + if ($node->var instanceof MethodCall) { + $shouldReverse = true; + } + + $calls[] = $node; + } + + return null; + } + ); + + return $shouldReverse ? array_reverse($calls) : $calls; + } +} diff --git a/rules/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector.php b/rules/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector.php index 0fa9b6e96..8cd038b47 100644 --- a/rules/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector.php +++ b/rules/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector.php @@ -18,7 +18,8 @@ use Rector\Symfony\Enum\CommandMethodName; use Rector\Symfony\Enum\SymfonyAttribute; use Rector\Symfony\Enum\SymfonyClass; -use Rector\Symfony\Symfony73\NodeAnalyzer\CommandArgumentsAndOptionsResolver; +use Rector\Symfony\Symfony73\NodeAnalyzer\CommandArgumentsResolver; +use Rector\Symfony\Symfony73\NodeAnalyzer\CommandOptionsResolver; use Rector\Symfony\Symfony73\NodeFactory\CommandInvokeParamsFactory; use Rector\Symfony\Symfony73\NodeTransformer\CommandUnusedInputOutputRemover; use Rector\Symfony\Symfony73\NodeTransformer\ConsoleOptionAndArgumentMethodCallVariableReplacer; @@ -41,7 +42,8 @@ final class InvokableCommandInputAttributeRector extends AbstractRector public function __construct( private readonly AttributeFinder $attributeFinder, - private readonly CommandArgumentsAndOptionsResolver $commandArgumentsAndOptionsResolver, + private readonly CommandArgumentsResolver $commandArgumentsResolver, + private readonly CommandOptionsResolver $commandOptionsResolver, private readonly CommandInvokeParamsFactory $commandInvokeParamsFactory, private readonly ConsoleOptionAndArgumentMethodCallVariableReplacer $consoleOptionAndArgumentMethodCallVariableReplacer, private readonly VisibilityManipulator $visibilityManipulator, @@ -153,12 +155,9 @@ public function refactor(Node $node): ?Class_ if ($configureClassMethod instanceof ClassMethod) { // 3. create arguments and options parameters - // @todo - $commandArguments = $this->commandArgumentsAndOptionsResolver->collectCommandArguments( - $configureClassMethod - ); + $commandArguments = $this->commandArgumentsResolver->resolve($configureClassMethod); - $commandOptions = $this->commandArgumentsAndOptionsResolver->collectCommandOptions($configureClassMethod); + $commandOptions = $this->commandOptionsResolver->resolve($configureClassMethod); // 4. remove configure() method $this->removeConfigureClassMethodIfNotUseful($node); diff --git a/rules/Symfony73/ValueObject/CommandArgument.php b/rules/Symfony73/ValueObject/CommandArgument.php index e96377b80..5b40cad97 100644 --- a/rules/Symfony73/ValueObject/CommandArgument.php +++ b/rules/Symfony73/ValueObject/CommandArgument.php @@ -10,8 +10,8 @@ { public function __construct( private Expr $name, - private Expr $mode, - private Expr $description + private ?Expr $mode, + private ?Expr $description ) { } @@ -20,12 +20,12 @@ public function getName(): Expr return $this->name; } - public function getMode(): Expr + public function getMode(): ?Expr { return $this->mode; } - public function getDescription(): Expr + public function getDescription(): ?Expr { return $this->description; } diff --git a/rules/Symfony73/ValueObject/CommandOption.php b/rules/Symfony73/ValueObject/CommandOption.php index ea6819e0a..af2ddd085 100644 --- a/rules/Symfony73/ValueObject/CommandOption.php +++ b/rules/Symfony73/ValueObject/CommandOption.php @@ -12,8 +12,6 @@ { public function __construct( private Expr $name, - // @todo type - // @todo default value ) { } From 312b6c23f7ff7022015a9d744b21c90ecef63844 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 10 Sep 2025 11:58:42 +0200 Subject: [PATCH 4/5] fix moving option metadata --- .../Fixture/name_with_hyphen.php.inc | 2 +- .../Fixture/some_command.php.inc | 2 +- .../some_command_with_method_chaining.php.inc | 2 +- .../Fixture/some_command_with_set_help.php.inc | 2 +- ...h_multiple_arguments_options_fluent.php.inc | 4 ++-- ...ultiple_arguments_options_no_fluent.php.inc | 4 ++-- .../Fixture/with_optional_argument.php.inc | 2 +- .../Fixture/with_override.php.inc | 2 +- .../NodeAnalyzer/CommandArgumentsResolver.php | 5 +++-- .../NodeAnalyzer/CommandOptionsResolver.php | 11 ++++++++--- .../NodeFactory/CommandInvokeParamsFactory.php | 13 +++++++++++++ rules/Symfony73/ValueObject/CommandOption.php | 18 ++++++++++++++++++ .../Fixture/command_remove_config.php.inc | 2 +- 13 files changed, 53 insertions(+), 16 deletions(-) diff --git a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/name_with_hyphen.php.inc b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/name_with_hyphen.php.inc index 809bb0501..98f7d0b13 100644 --- a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/name_with_hyphen.php.inc +++ b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/name_with_hyphen.php.inc @@ -44,7 +44,7 @@ use Symfony\Component\Console\Output\OutputInterface; class NameWithHyphen { public function __invoke(#[\Symfony\Component\Console\Attribute\Argument(name: 'argument-with-hyphen', description: 'Argument description')] - ?string $argument_with_hyphen, #[\Symfony\Component\Console\Attribute\Option] + ?string $argument_with_hyphen, #[\Symfony\Component\Console\Attribute\Option(name: 'option-with-hyphen')] $option_with_hyphen): int { $argument = $argument_with_hyphen; diff --git a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/some_command.php.inc b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/some_command.php.inc index efb0f8ff1..6679395fe 100644 --- a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/some_command.php.inc +++ b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/some_command.php.inc @@ -46,7 +46,7 @@ use Symfony\Component\Console\Input\InputOption; final class SomeCommand { public function __invoke(#[\Symfony\Component\Console\Attribute\Argument(name: 'argument', description: 'Argument description')] - string $argument, #[\Symfony\Component\Console\Attribute\Option] + string $argument, #[\Symfony\Component\Console\Attribute\Option(name: 'option', shortcut: 'o', mode: InputOption::VALUE_NONE, description: 'Option description')] $option): int { $someArgument = $argument; diff --git a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/some_command_with_method_chaining.php.inc b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/some_command_with_method_chaining.php.inc index 8eb7a665b..6f1ab9b6a 100644 --- a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/some_command_with_method_chaining.php.inc +++ b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/some_command_with_method_chaining.php.inc @@ -47,7 +47,7 @@ use Symfony\Component\Console\Input\InputOption; final class SomeCommandWithMethodChaining { public function __invoke(#[\Symfony\Component\Console\Attribute\Argument(name: 'argument', description: 'Argument description')] - string $argument, #[\Symfony\Component\Console\Attribute\Option] + string $argument, #[\Symfony\Component\Console\Attribute\Option(name: 'option', shortcut: 'o', mode: InputOption::VALUE_NONE, description: 'Option description')] $option): int { $someArgument = $argument; diff --git a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/some_command_with_set_help.php.inc b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/some_command_with_set_help.php.inc index 23732edcb..d93b8d354 100644 --- a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/some_command_with_set_help.php.inc +++ b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/some_command_with_set_help.php.inc @@ -52,7 +52,7 @@ final class SomeCommandWithSetHelp } public function __invoke(#[\Symfony\Component\Console\Attribute\Argument(name: 'argument', description: 'Argument description')] - string $argument, #[\Symfony\Component\Console\Attribute\Option] + string $argument, #[\Symfony\Component\Console\Attribute\Option(name: 'option', shortcut: 'o', mode: InputOption::VALUE_NONE, description: 'Option description')] $option): int { $someArgument = $argument; diff --git a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_multiple_arguments_options_fluent.php.inc b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_multiple_arguments_options_fluent.php.inc index fba5f55be..bd4ea2901 100644 --- a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_multiple_arguments_options_fluent.php.inc +++ b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_multiple_arguments_options_fluent.php.inc @@ -56,9 +56,9 @@ final class WithMultipleArgumentsOptionsFluent string $argument1, #[\Symfony\Component\Console\Attribute\Argument(name: 'argument2', description: 'Argument2 description')] string $argument2, - #[\Symfony\Component\Console\Attribute\Option] + #[\Symfony\Component\Console\Attribute\Option(name: 'option1', shortcut: 'o', mode: InputOption::VALUE_NONE, description: 'Option1 description')] $option1, - #[\Symfony\Component\Console\Attribute\Option] + #[\Symfony\Component\Console\Attribute\Option(name: 'option2', shortcut: 'p', mode: InputOption::VALUE_NONE, description: 'Option2 description')] $option2 ): int { diff --git a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_multiple_arguments_options_no_fluent.php.inc b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_multiple_arguments_options_no_fluent.php.inc index ecb3a4c31..5baf04887 100644 --- a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_multiple_arguments_options_no_fluent.php.inc +++ b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_multiple_arguments_options_no_fluent.php.inc @@ -61,9 +61,9 @@ final class WithMultipleArgumentsOptionsNotFluent string $argument1, #[\Symfony\Component\Console\Attribute\Argument(name: 'argument2', description: 'Argument2 description')] string $argument2, - #[\Symfony\Component\Console\Attribute\Option] + #[\Symfony\Component\Console\Attribute\Option(name: 'option1', shortcut: 'o', mode: InputOption::VALUE_NONE, description: 'Option1 description')] $option1, - #[\Symfony\Component\Console\Attribute\Option] + #[\Symfony\Component\Console\Attribute\Option(name: 'option2', shortcut: 'p', mode: InputOption::VALUE_NONE, description: 'Option2 description')] $option2 ): int { diff --git a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_optional_argument.php.inc b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_optional_argument.php.inc index a1fa598a1..f55bae24b 100644 --- a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_optional_argument.php.inc +++ b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_optional_argument.php.inc @@ -46,7 +46,7 @@ use Symfony\Component\Console\Input\InputOption; final class WithOptionalArgument { public function __invoke(#[\Symfony\Component\Console\Attribute\Argument(name: 'argument', description: 'Argument description')] - ?string $argument, #[\Symfony\Component\Console\Attribute\Option] + ?string $argument, #[\Symfony\Component\Console\Attribute\Option(name: 'option', shortcut: 'o', mode: InputOption::VALUE_NONE, description: 'Option description')] $option): int { $someArgument = $argument; diff --git a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_override.php.inc b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_override.php.inc index 17b0e765e..9ed55b340 100644 --- a/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_override.php.inc +++ b/rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/with_override.php.inc @@ -50,7 +50,7 @@ final class WithOverride public function __invoke( #[\Symfony\Component\Console\Attribute\Argument(name: 'argument', description: 'Argument description')] string $argument, - #[\Symfony\Component\Console\Attribute\Option] + #[\Symfony\Component\Console\Attribute\Option(name: 'option', shortcut: 'o', mode: InputOption::VALUE_NONE, description: 'Option description')] $option ): int { diff --git a/rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php b/rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php index 2bd5d528f..cdef3f0b1 100644 --- a/rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php +++ b/rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php @@ -5,6 +5,7 @@ namespace Rector\Symfony\Symfony73\NodeAnalyzer; use PhpParser\Node; +use PhpParser\Node\Expr; use PhpParser\Node\Stmt\ClassMethod; use Rector\Exception\ShouldNotHappenException; use Rector\PhpParser\Node\Value\ValueResolver; @@ -52,7 +53,7 @@ public function resolve(ClassMethod $configureClassMethod): array /** * @param Node\Arg[] $addArgumentArgs */ - private function resolveArgumentModeExpr(array $addArgumentArgs): ?Node\Expr + private function resolveArgumentModeExpr(array $addArgumentArgs): ?Expr { if (! isset($addArgumentArgs[1])) { return null; @@ -70,7 +71,7 @@ private function resolveArgumentModeExpr(array $addArgumentArgs): ?Node\Expr /** * @param Node\Arg[] $addArgumentArgs */ - private function resolveArgumentDescriptionExpr(array $addArgumentArgs): ?Node\Expr + private function resolveArgumentDescriptionExpr(array $addArgumentArgs): ?Expr { if (! isset($addArgumentArgs[2])) { return null; diff --git a/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php b/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php index 06a031114..c11ce100f 100644 --- a/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php +++ b/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php @@ -24,7 +24,8 @@ public function resolve(ClassMethod $configureClassMethod): array { $addOptionMethodCalls = $this->methodCallFinder->find($configureClassMethod, 'addOption'); - $commandOptionMetadatas = []; + $commandOptions = []; + foreach ($addOptionMethodCalls as $addOptionMethodCall) { $addOptionArgs = $addOptionMethodCall->getArgs(); @@ -34,9 +35,13 @@ public function resolve(ClassMethod $configureClassMethod): array throw new ShouldNotHappenException('Option name is required'); } - $commandOptionMetadatas[] = new CommandOption($nameArgValue); + $shortcutExpr = $addOptionArgs[1]?->value ?? null; + $modeExpr = $addOptionArgs[2]?->value ?? null; + $descriptionExpr = $addOptionArgs[3]?->value ?? null; + + $commandOptions[] = new CommandOption($nameArgValue, $shortcutExpr, $modeExpr, $descriptionExpr); } - return $commandOptionMetadatas; + return $commandOptions; } } diff --git a/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php b/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php index a066cc8ef..65608c6c3 100644 --- a/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php +++ b/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php @@ -7,6 +7,7 @@ use PhpParser\Node\Arg; use PhpParser\Node\Attribute; use PhpParser\Node\AttributeGroup; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Identifier; use PhpParser\Node\Name\FullyQualified; @@ -93,6 +94,18 @@ private function createOptionParams(array $commandOptions): array $optionArgs = [new Arg(value: $commandOption->getName(), name: new Identifier('name'))]; + if ($commandOption->getShortcut() instanceof Expr) { + $optionArgs[] = new Arg(value: $commandOption->getShortcut(), name: new Identifier('shortcut')); + } + + if ($commandOption->getMode() instanceof Expr) { + $optionArgs[] = new Arg(value: $commandOption->getMode(), name: new Identifier('mode')); + } + + if ($commandOption->getDescription() instanceof Expr) { + $optionArgs[] = new Arg(value: $commandOption->getDescription(), name: new Identifier('description')); + } + $optionParam->attrGroups[] = new AttributeGroup([ new Attribute(new FullyQualified(SymfonyAttribute::COMMAND_OPTION), $optionArgs), ]); diff --git a/rules/Symfony73/ValueObject/CommandOption.php b/rules/Symfony73/ValueObject/CommandOption.php index af2ddd085..266036e82 100644 --- a/rules/Symfony73/ValueObject/CommandOption.php +++ b/rules/Symfony73/ValueObject/CommandOption.php @@ -12,6 +12,9 @@ { public function __construct( private Expr $name, + private ?Expr $shortcut, + private ?Expr $mode, + private ?Expr $description, ) { } @@ -20,6 +23,21 @@ public function getName(): Expr return $this->name; } + public function getShortcut(): ?Expr + { + return $this->shortcut; + } + + public function getMode(): ?Expr + { + return $this->mode; + } + + public function getDescription(): ?Expr + { + return $this->description; + } + public function getStringName(): string { if ($this->name instanceof String_) { diff --git a/tests/Set/Symfony73/Fixture/command_remove_config.php.inc b/tests/Set/Symfony73/Fixture/command_remove_config.php.inc index 56b0ac90f..7986383e0 100644 --- a/tests/Set/Symfony73/Fixture/command_remove_config.php.inc +++ b/tests/Set/Symfony73/Fixture/command_remove_config.php.inc @@ -57,7 +57,7 @@ TXT)] final class SomeCommand { public function __invoke(#[\Symfony\Component\Console\Attribute\Argument(name: 'argument', description: 'Argument description')] - string $argument, #[\Symfony\Component\Console\Attribute\Option(name: 'option', shortcut: 'o', description: 'Option description', mode: \Symfony\Component\Console\Input\InputOption::VALUE_NONE)] + string $argument, #[\Symfony\Component\Console\Attribute\Option(name: 'option', shortcut: 'o', mode: InputOption::VALUE_NONE, description: 'Option description')] $option): int { $someArgument = $argument; From 701b76c977e61e554dcaf388f6ce01ba3368acde Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 10 Sep 2025 12:01:49 +0200 Subject: [PATCH 5/5] Tidy up --- composer.json | 1 + phpstan.neon | 1 + .../NodeAnalyzer/CommandArgumentsResolver.php | 54 +------------------ .../NodeAnalyzer/CommandOptionsResolver.php | 6 +-- 4 files changed, 7 insertions(+), 55 deletions(-) diff --git a/composer.json b/composer.json index fb2283b45..a6ff73092 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "symfony/security-http": "^6.4", "symfony/validator": "^6.4", "symfony/web-link": "^6.4", + "symplify/phpstan-extensions": "^12.0", "symplify/phpstan-rules": "^14.6", "symplify/vendor-patches": "^11.3", "tomasvotruba/class-leak": "^2.0", diff --git a/phpstan.neon b/phpstan.neon index 39fa6b842..a3fb14352 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,6 +3,7 @@ rules: parameters: level: 8 + errorFormat: symplify reportUnmatchedIgnoredErrors: false treatPhpDocTypesAsCertain: false diff --git a/rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php b/rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php index cdef3f0b1..613193a42 100644 --- a/rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php +++ b/rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php @@ -4,18 +4,13 @@ namespace Rector\Symfony\Symfony73\NodeAnalyzer; -use PhpParser\Node; -use PhpParser\Node\Expr; use PhpParser\Node\Stmt\ClassMethod; -use Rector\Exception\ShouldNotHappenException; -use Rector\PhpParser\Node\Value\ValueResolver; use Rector\Symfony\Symfony73\NodeFinder\MethodCallFinder; use Rector\Symfony\Symfony73\ValueObject\CommandArgument; final readonly class CommandArgumentsResolver { public function __construct( - private ValueResolver $valueResolver, private MethodCallFinder $methodCallFinder, ) { } @@ -31,58 +26,13 @@ public function resolve(ClassMethod $configureClassMethod): array foreach ($addArgumentMethodCalls as $addArgumentMethodCall) { $addArgumentArgs = $addArgumentMethodCall->getArgs(); - $optionName = $this->valueResolver->getValue($addArgumentArgs[0]->value); - if ($optionName === null) { - // we need string value, otherwise param will not have a name - throw new ShouldNotHappenException('Argument name is required'); - } - - $argumentModeExpr = $this->resolveArgumentModeExpr($addArgumentArgs); - $argumentDescriptionExpr = $this->resolveArgumentDescriptionExpr($addArgumentArgs); - $commandArguments[] = new CommandArgument( $addArgumentArgs[0]->value, - $argumentModeExpr, - $argumentDescriptionExpr + $addArgumentArgs[1]->value ?? null, + $addArgumentArgs[2]->value ?? null, ); } return $commandArguments; } - - /** - * @param Node\Arg[] $addArgumentArgs - */ - private function resolveArgumentModeExpr(array $addArgumentArgs): ?Expr - { - if (! isset($addArgumentArgs[1])) { - return null; - } - - $mode = $this->valueResolver->getValue($addArgumentArgs[1]->value); - if ($mode !== null && ! is_numeric($mode)) { - // we need numeric value or null, otherwise param will not have a name - throw new ShouldNotHappenException('Argument mode is required to be null or numeric'); - } - - return $addArgumentArgs[1]->value; - } - - /** - * @param Node\Arg[] $addArgumentArgs - */ - private function resolveArgumentDescriptionExpr(array $addArgumentArgs): ?Expr - { - if (! isset($addArgumentArgs[2])) { - return null; - } - - $description = $this->valueResolver->getValue($addArgumentArgs[2]->value); - if (! is_string($description)) { - // we need string value, otherwise param will not have a name - throw new ShouldNotHappenException('Argument description is required'); - } - - return $addArgumentArgs[2]->value; - } } diff --git a/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php b/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php index c11ce100f..085d17464 100644 --- a/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php +++ b/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php @@ -35,9 +35,9 @@ public function resolve(ClassMethod $configureClassMethod): array throw new ShouldNotHappenException('Option name is required'); } - $shortcutExpr = $addOptionArgs[1]?->value ?? null; - $modeExpr = $addOptionArgs[2]?->value ?? null; - $descriptionExpr = $addOptionArgs[3]?->value ?? null; + $shortcutExpr = $addOptionArgs[1]->value ?? null; + $modeExpr = $addOptionArgs[2]->value ?? null; + $descriptionExpr = $addOptionArgs[3]->value ?? null; $commandOptions[] = new CommandOption($nameArgValue, $shortcutExpr, $modeExpr, $descriptionExpr); }