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-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/CommandArgumentsAndOptionsResolver.php b/rules/Symfony73/NodeAnalyzer/CommandArgumentsAndOptionsResolver.php deleted file mode 100644 index c5ef8c540..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($optionName); - } - - 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..613193a42 --- /dev/null +++ b/rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php @@ -0,0 +1,38 @@ +methodCallFinder->find($configureClassMethod, 'addArgument'); + + $commandArguments = []; + foreach ($addArgumentMethodCalls as $addArgumentMethodCall) { + $addArgumentArgs = $addArgumentMethodCall->getArgs(); + + $commandArguments[] = new CommandArgument( + $addArgumentArgs[0]->value, + $addArgumentArgs[1]->value ?? null, + $addArgumentArgs[2]->value ?? null, + ); + } + + return $commandArguments; + } +} diff --git a/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php b/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php new file mode 100644 index 000000000..085d17464 --- /dev/null +++ b/rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php @@ -0,0 +1,47 @@ +methodCallFinder->find($configureClassMethod, 'addOption'); + + $commandOptions = []; + + 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'); + } + + $shortcutExpr = $addOptionArgs[1]->value ?? null; + $modeExpr = $addOptionArgs[2]->value ?? null; + $descriptionExpr = $addOptionArgs[3]->value ?? null; + + $commandOptions[] = new CommandOption($nameArgValue, $shortcutExpr, $modeExpr, $descriptionExpr); + } + + return $commandOptions; + } +} diff --git a/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php b/rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php index 323ebef07..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; @@ -86,13 +87,27 @@ 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 + + $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)), + 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 b6880612b..266036e82 100644 --- a/rules/Symfony73/ValueObject/CommandOption.php +++ b/rules/Symfony73/ValueObject/CommandOption.php @@ -4,17 +4,46 @@ 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, - // @todo type - // @todo default value + private Expr $name, + private ?Expr $shortcut, + private ?Expr $mode, + private ?Expr $description, ) { } - public function getName(): string + 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_) { + return $this->name->value; + } + + throw new NotImplementedYetException(sprintf('Add more options to "%s"', self::class)); + } } diff --git a/tests/Set/Symfony73/Fixture/command_remove_config.php.inc b/tests/Set/Symfony73/Fixture/command_remove_config.php.inc index a56741266..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] + string $argument, #[\Symfony\Component\Console\Attribute\Option(name: 'option', shortcut: 'o', mode: InputOption::VALUE_NONE, description: 'Option description')] $option): int { $someArgument = $argument;