Skip to content

Commit 00c1f25

Browse files
authored
[symfony 7.3] Add InvokableCommandRector - kick off (#707)
* [symfony 7.3] Add InvokableCommandRector * phsptan fixes
1 parent d4e5a0f commit 00c1f25

File tree

16 files changed

+604
-40
lines changed

16 files changed

+604
-40
lines changed

composer.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,24 @@
88
"ext-xml": "*"
99
},
1010
"require-dev": {
11+
"phpecs/phpecs": "^2.0.1",
1112
"phpstan/extension-installer": "^1.4",
1213
"phpstan/phpstan": "^2.1.8",
1314
"phpstan/phpstan-webmozart-assert": "^2.0",
1415
"phpunit/phpunit": "^11.4",
1516
"rector/rector-src": "dev-main",
17+
"rector/type-perfect": "^2.0",
1618
"symfony/config": "^6.4",
1719
"symfony/dependency-injection": "^6.4",
18-
"symfony/http-kernel": "~6.3",
20+
"symfony/http-kernel": "^6.4",
1921
"symfony/routing": "^6.4",
2022
"symfony/security-core": "^6.4",
2123
"symfony/security-http": "^6.4",
2224
"symfony/validator": "^6.4",
23-
"symplify/easy-coding-standard": "^12.3",
2425
"symplify/vendor-patches": "^11.3",
25-
"tomasvotruba/class-leak": "^1.0",
26+
"tomasvotruba/class-leak": "^2.0",
27+
"tomasvotruba/type-coverage": "^2.0",
28+
"tomasvotruba/unused-public": "^2.0",
2629
"tracy/tracy": "^2.10"
2730
},
2831
"autoload": {

config/sets/symfony/symfony73.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\Symfony\Symfony73\Rector\Class_\InvokableCommandRector;
7+
8+
// @see https://github.com/symfony/symfony/blame/7.3/UPGRADE-7.3.md
9+
10+
return RectorConfig::configure()
11+
->withRules([InvokableCommandRector::class]);

phpstan.neon

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ parameters:
22
level: 8
33

44
reportUnmatchedIgnoredErrors: false
5-
65
treatPhpDocTypesAsCertain: false
76

87
paths:
@@ -12,18 +11,17 @@ parameters:
1211
- rules
1312
- rules-tests
1413

15-
# to be enabled later once rector upgraded to use phpstan v2
16-
# # https://github.com/rectorphp/type-perfect/
17-
# type_perfect:
18-
# no_mixed: true
19-
# null_over_false: true
20-
# narrow_param: true
21-
# narrow_return: true
14+
# https://github.com/rectorphp/type-perfect/
15+
type_perfect:
16+
no_mixed: true
17+
null_over_false: true
18+
narrow_param: true
19+
narrow_return: true
2220

23-
# unused_public:
24-
# constants: true
25-
# methods: true
26-
# properties: true
21+
unused_public:
22+
constants: true
23+
methods: true
24+
properties: true
2725

2826
scanDirectories:
2927
- stubs
@@ -54,16 +52,12 @@ parameters:
5452
- '#Doing instanceof PHPStan\\Type\\.+ is error\-prone and deprecated#'
5553

5654
# phpstan instanceof
57-
-
58-
identifier: phpstanApi.instanceofAssumption
59-
60-
-
61-
identifier: phpstanApi.varTagAssumption
55+
- identifier: argument.type
56+
- identifier: assign.propertyType
6257

63-
-
64-
identifier: argument.type
58+
- '#::provideMinPhpVersion\(\) never returns \d+ so it can be removed from the return type#'
6559

60+
# node finder
6661
-
67-
identifier: assign.propertyType
68-
69-
- '#::provideMinPhpVersion\(\) never returns \d+ so it can be removed from the return type#'
62+
identifier: return.type
63+
path: rules/Symfony73/NodeAnalyzer/CommandArgumentsAndOptionsResolver.php
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandRector\Fixture;
4+
5+
use Symfony\Component\Console\Attribute\AsCommand;
6+
use Symfony\Component\Console\Command\Command;
7+
use Symfony\Component\Console\Input\InputInterface;
8+
use Symfony\Component\Console\Output\OutputInterface;
9+
use Symfony\Component\Console\Input\InputArgument;
10+
use Symfony\Component\Console\Input\InputOption;
11+
12+
#[AsCommand(name: 'some_name')]
13+
final class SomeCommand extends Command
14+
{
15+
public function configure()
16+
{
17+
$this->addArgument('argument', InputArgument::REQUIRED, 'Argument description');
18+
$this->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
19+
}
20+
21+
public function execute(InputInterface $input, OutputInterface $output): int
22+
{
23+
$someArgument = $input->getArgument('argument');
24+
$someOption = $input->getOption('option');
25+
26+
// ...
27+
28+
return 1;
29+
}
30+
}
31+
32+
?>
33+
-----
34+
<?php
35+
36+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandRector\Fixture;
37+
38+
use Symfony\Component\Console\Attribute\AsCommand;
39+
use Symfony\Component\Console\Command\Command;
40+
use Symfony\Component\Console\Input\InputInterface;
41+
use Symfony\Component\Console\Output\OutputInterface;
42+
use Symfony\Component\Console\Input\InputArgument;
43+
use Symfony\Component\Console\Input\InputOption;
44+
45+
#[AsCommand(name: 'some_name')]
46+
final class SomeCommand
47+
{
48+
public function __invoke(#[\Symfony\Component\Console\Attribute\Command\Argument]
49+
string $argument, #[\Symfony\Component\Console\Attribute\Command\Option]
50+
$option): int
51+
{
52+
$someArgument = $argument;
53+
$someOption = $option;
54+
55+
// ...
56+
57+
return 1;
58+
}
59+
}
60+
61+
?>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class InvokableCommandRectorTest extends AbstractRectorTestCase
12+
{
13+
#[DataProvider('provideData')]
14+
public function test(string $filePath): void
15+
{
16+
$this->doTestFile($filePath);
17+
}
18+
19+
public static function provideData(): Iterator
20+
{
21+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\Symfony\Symfony73\Rector\Class_\InvokableCommandRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(InvokableCommandRector::class);
10+
};

rules/Symfony61/Rector/Class_/CommandConfigureToAttributeRector.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
use PHPStan\Type\ObjectType;
2020
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
2121
use Rector\Rector\AbstractRector;
22-
use Rector\Symfony\Enum\SymfonyAnnotation;
22+
use Rector\Symfony\Enum\SymfonyAttribute;
23+
use Rector\Symfony\Enum\SymfonyClass;
2324
use Rector\ValueObject\PhpVersionFeature;
2425
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
2526
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
2627
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
2728

2829
/**
29-
* @changelog https://symfony.com/doc/current/console.html#registering-the-command
30+
* @see https://symfony.com/doc/current/console.html#registering-the-command
3031
*
3132
* @see \Rector\Symfony\Tests\Symfony61\Rector\Class_\CommandConfigureToAttributeRector\CommandConfigureToAttributeRectorTest
3233
*/
@@ -102,11 +103,11 @@ public function refactor(Node $node): ?Node
102103
return null;
103104
}
104105

105-
if (! $this->reflectionProvider->hasClass(SymfonyAnnotation::AS_COMMAND)) {
106+
if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
106107
return null;
107108
}
108109

109-
if (! $this->isObjectType($node, new ObjectType('Symfony\\Component\\Console\\Command\\Command'))) {
110+
if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
110111
return null;
111112
}
112113

@@ -120,7 +121,7 @@ public function refactor(Node $node): ?Node
120121
$attributeArgs = [];
121122
foreach ($node->attrGroups as $attrGroup) {
122123
foreach ($attrGroup->attrs as $attribute) {
123-
if (! $this->nodeNameResolver->isName($attribute->name, SymfonyAnnotation::AS_COMMAND)) {
124+
if (! $this->nodeNameResolver->isName($attribute->name, SymfonyAttribute::AS_COMMAND)) {
124125
continue;
125126
}
126127

@@ -139,7 +140,7 @@ public function refactor(Node $node): ?Node
139140
}
140141

141142
if (! $asCommandAttribute instanceof Attribute) {
142-
$asCommandAttributeGroup = $this->phpAttributeGroupFactory->createFromClass(SymfonyAnnotation::AS_COMMAND);
143+
$asCommandAttributeGroup = $this->phpAttributeGroupFactory->createFromClass(SymfonyAttribute::AS_COMMAND);
143144

144145
$asCommandAttribute = $asCommandAttributeGroup->attrs[0];
145146

rules/Symfony61/Rector/Class_/CommandPropertyToAttributeRector.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
use Rector\Doctrine\NodeAnalyzer\AttributeFinder;
1818
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
1919
use Rector\Rector\AbstractRector;
20-
use Rector\Symfony\Enum\SymfonyAnnotation;
20+
use Rector\Symfony\Enum\SymfonyAttribute;
2121
use Rector\Symfony\Enum\SymfonyClass;
2222
use Rector\ValueObject\PhpVersionFeature;
2323
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
@@ -91,7 +91,7 @@ public function refactor(Node $node): ?Node
9191
}
9292

9393
// does attribute already exist?
94-
if (! $this->reflectionProvider->hasClass(SymfonyAnnotation::AS_COMMAND)) {
94+
if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
9595
return null;
9696
}
9797

@@ -104,7 +104,7 @@ public function refactor(Node $node): ?Node
104104

105105
$existingAsCommandAttribute = $this->attributeFinder->findAttributeByClass(
106106
$node,
107-
SymfonyAnnotation::AS_COMMAND
107+
SymfonyAttribute::AS_COMMAND
108108
);
109109

110110
$attributeArgs = $this->createAttributeArgs($defaultNameExpr, $defaultDescriptionExpr);
@@ -126,7 +126,7 @@ private function createAttributeGroupAsCommand(array $args): AttributeGroup
126126
{
127127
Assert::allIsInstanceOf($args, Arg::class);
128128

129-
$attributeGroup = $this->phpAttributeGroupFactory->createFromClass(SymfonyAnnotation::AS_COMMAND);
129+
$attributeGroup = $this->phpAttributeGroupFactory->createFromClass(SymfonyAttribute::AS_COMMAND);
130130
$attributeGroup->attrs[0]->args = $args;
131131

132132
return $attributeGroup;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\Symfony73\NodeAnalyzer;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PhpParser\Node\Identifier;
10+
use PhpParser\Node\Scalar\String_;
11+
use PhpParser\Node\Stmt\ClassMethod;
12+
use PhpParser\NodeFinder;
13+
use Rector\Exception\ShouldNotHappenException;
14+
use Rector\Symfony\Symfony73\ValueObject\CommandArgument;
15+
use Rector\Symfony\Symfony73\ValueObject\CommandOption;
16+
17+
final class CommandArgumentsAndOptionsResolver
18+
{
19+
/**
20+
* @return CommandArgument[]
21+
*/
22+
public function collectCommandArguments(ClassMethod $configureClassMethod): array
23+
{
24+
$addArgumentMethodCalls = $this->findMethodCallsByName($configureClassMethod, 'addArgument');
25+
26+
$commandArguments = [];
27+
foreach ($addArgumentMethodCalls as $addArgumentMethodCall) {
28+
// @todo extract name, type and requirements
29+
$addArgumentArgs = $addArgumentMethodCall->getArgs();
30+
31+
$nameArgValue = $addArgumentArgs[0]->value;
32+
if (! $nameArgValue instanceof String_) {
33+
// we need string value, otherwise param will not have a name
34+
throw new ShouldNotHappenException('Argument name is required');
35+
}
36+
37+
$optionName = $nameArgValue->value;
38+
39+
$commandArguments[] = new CommandArgument($optionName);
40+
}
41+
42+
return $commandArguments;
43+
}
44+
45+
/**
46+
* @return CommandOption[]
47+
*/
48+
public function collectCommandOptions(ClassMethod $configureClassMethod): array
49+
{
50+
$addOptionMethodCalls = $this->findMethodCallsByName($configureClassMethod, 'addOption');
51+
52+
$commandOptionMetadatas = [];
53+
foreach ($addOptionMethodCalls as $addOptionMethodCall) {
54+
// @todo extract name, type and requirements
55+
$addOptionArgs = $addOptionMethodCall->getArgs();
56+
57+
$nameArgValue = $addOptionArgs[0]->value;
58+
if (! $nameArgValue instanceof String_) {
59+
// we need string value, otherwise param will not have a name
60+
throw new ShouldNotHappenException('Option name is required');
61+
}
62+
63+
$optionName = $nameArgValue->value;
64+
65+
$commandOptionMetadatas[] = new CommandOption($optionName);
66+
}
67+
68+
return $commandOptionMetadatas;
69+
}
70+
71+
/**
72+
* @return MethodCall[]
73+
*/
74+
private function findMethodCallsByName(ClassMethod $classMethod, string $desiredMethodName): array
75+
{
76+
$nodeFinder = new NodeFinder();
77+
78+
return $nodeFinder->find($classMethod, function (Node $node) use ($desiredMethodName): bool {
79+
if (! $node instanceof MethodCall) {
80+
return false;
81+
}
82+
83+
if (! $node->name instanceof Identifier) {
84+
return false;
85+
}
86+
87+
return $node->name->toString() === $desiredMethodName;
88+
});
89+
}
90+
}

0 commit comments

Comments
 (0)