Skip to content

Commit 31483de

Browse files
committed
[Symfony 7.3] add constant and test for 7.3 and test help to attribute
1 parent 9a4e2c8 commit 31483de

File tree

12 files changed

+542
-2
lines changed

12 files changed

+542
-2
lines changed

config/sets/symfony/symfony73.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
declare(strict_types=1);
44

55
use Rector\Config\RectorConfig;
6+
use Rector\Symfony\Symfony73\Rector\Class_\CommandHelpToAttributeRector;
67
use Rector\Symfony\Symfony73\Rector\Class_\InvokableCommandRector;
78

89
// @see https://github.com/symfony/symfony/blame/7.3/UPGRADE-7.3.md
910

1011
return RectorConfig::configure()
11-
->withRules([InvokableCommandRector::class]);
12+
->withRules([CommandHelpToAttributeRector::class, InvokableCommandRector::class]);
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_\CommandHelpToAttributeRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class CommandHelpToAttributeRectorTest 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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\Fixture;
4+
5+
use Symfony\Component\Console\Attribute\AsCommand;
6+
use Symfony\Component\Console\Command\Command;
7+
8+
#[AsCommand(name: 'some_name')]
9+
final class SomeCommand extends Command
10+
{
11+
public function configure()
12+
{
13+
$this->setHelp('Some help text');
14+
}
15+
}
16+
17+
?>
18+
-----
19+
<?php
20+
21+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\Fixture;
22+
23+
use Symfony\Component\Console\Attribute\AsCommand;
24+
use Symfony\Component\Console\Command\Command;
25+
26+
#[AsCommand(name: 'some_name', help: '<<<TXT Some help text TXT')]
27+
final class SomeCommand extends Command
28+
{
29+
public function configure()
30+
{
31+
}
32+
}
33+
34+
?>
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_\CommandHelpToAttributeRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(CommandHelpToAttributeRector::class);
10+
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 SomeCommandWithSetHelp extends Command
14+
{
15+
public function configure()
16+
{
17+
$this->setHelp('argument');
18+
$this->addArgument('argument', InputArgument::REQUIRED, 'Argument description');
19+
$this->addOption('option', 'o', InputOption::VALUE_NONE, 'Option description');
20+
}
21+
22+
public function execute(InputInterface $input, OutputInterface $output): int
23+
{
24+
$someArgument = $input->getArgument('argument');
25+
$someOption = $input->getOption('option');
26+
27+
// ...
28+
29+
return 1;
30+
}
31+
}
32+
33+
?>
34+
-----
35+
<?php
36+
37+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandRector\Fixture;
38+
39+
use Symfony\Component\Console\Attribute\AsCommand;
40+
use Symfony\Component\Console\Command\Command;
41+
use Symfony\Component\Console\Input\InputInterface;
42+
use Symfony\Component\Console\Output\OutputInterface;
43+
use Symfony\Component\Console\Input\InputArgument;
44+
use Symfony\Component\Console\Input\InputOption;
45+
46+
#[AsCommand(name: 'some_name')]
47+
final class SomeCommandWithSetHelp
48+
{
49+
public function configure()
50+
{
51+
$this->setHelp('argument');
52+
}
53+
54+
public function __invoke(#[\Symfony\Component\Console\Attribute\Command\Argument]
55+
string $argument, #[\Symfony\Component\Console\Attribute\Command\Option]
56+
$option): int
57+
{
58+
$someArgument = $argument;
59+
$someOption = $option;
60+
61+
// ...
62+
63+
return 1;
64+
}
65+
}
66+
67+
?>
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\Symfony73\Rector\Class_;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Arg;
9+
use PhpParser\Node\Attribute;
10+
use PhpParser\Node\Expr;
11+
use PhpParser\Node\Expr\MethodCall;
12+
use PhpParser\Node\Expr\Variable;
13+
use PhpParser\Node\Identifier;
14+
use PhpParser\Node\Scalar\String_;
15+
use PhpParser\Node\Stmt;
16+
use PhpParser\Node\Stmt\Class_;
17+
use PhpParser\Node\Stmt\ClassMethod;
18+
use PhpParser\Node\Stmt\Expression;
19+
use PHPStan\Reflection\ReflectionProvider;
20+
use PHPStan\Type\ObjectType;
21+
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
22+
use Rector\Rector\AbstractRector;
23+
use Rector\Symfony\Enum\CommandMethodName;
24+
use Rector\Symfony\Enum\SymfonyAttribute;
25+
use Rector\Symfony\Enum\SymfonyClass;
26+
use Rector\ValueObject\PhpVersionFeature;
27+
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
28+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
29+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
30+
31+
/**
32+
* @see https://symfony.com/doc/current/console.html#help-message
33+
*
34+
* @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\CommandHelpToAttributeRector\CommandHelpToAttributeRectorTest
35+
*/
36+
final class CommandHelpToAttributeRector extends AbstractRector implements MinPhpVersionInterface
37+
{
38+
public function __construct(
39+
private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
40+
private readonly ReflectionProvider $reflectionProvider,
41+
) {
42+
}
43+
44+
public function provideMinPhpVersion(): int
45+
{
46+
return PhpVersionFeature::ATTRIBUTES;
47+
}
48+
49+
public function getRuleDefinition(): RuleDefinition
50+
{
51+
return new RuleDefinition(
52+
'Moves $this->setHelp() to the "help" named argument of #[AsCommand]',
53+
[
54+
new CodeSample(
55+
<<<'CODE_SAMPLE'
56+
use Symfony\Component\Console\Command\Command;
57+
58+
final class SomeCommand extends Command
59+
{
60+
protected function configure(): void
61+
{
62+
$this->setHelp('Some help text');
63+
}
64+
}
65+
CODE_SAMPLE
66+
,
67+
<<<'CODE_SAMPLE'
68+
use Symfony\Component\Console\Attribute\AsCommand;
69+
use Symfony\Component\Console\Command\Command;
70+
71+
#[AsCommand(name: 'app:some', help: '<<<TXT Some help text TXT')]
72+
final class SomeCommand extends Command
73+
{
74+
}
75+
CODE_SAMPLE
76+
),
77+
]
78+
);
79+
}
80+
81+
/**
82+
* @return array<class-string<Node>>
83+
*/
84+
public function getNodeTypes(): array
85+
{
86+
return [Class_::class];
87+
}
88+
89+
/**
90+
* @param Class_ $node
91+
*/
92+
public function refactor(Node $node): ?Node
93+
{
94+
if ($node->isAbstract()) {
95+
return null;
96+
}
97+
98+
if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
99+
return null;
100+
}
101+
102+
if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
103+
return null;
104+
}
105+
106+
$asCommandAttribute = $this->ensureAsCommandAttribute($node);
107+
108+
foreach ($asCommandAttribute->args as $arg) {
109+
if ($arg->name?->toString() === 'help') {
110+
return $node;
111+
}
112+
}
113+
114+
$configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
115+
if (! $configureClassMethod instanceof ClassMethod) {
116+
return null;
117+
}
118+
119+
$helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
120+
if (! $helpExpr instanceof String_) {
121+
return null;
122+
}
123+
124+
$wrappedHelp = new String_(
125+
'<<<TXT ' . $helpExpr->value . ' TXT'
126+
);
127+
128+
$asCommandAttribute->args[] = new Arg($wrappedHelp, false, false, [], new Identifier('help'));
129+
130+
if ($configureClassMethod->stmts === []) {
131+
unset($configureClassMethod);
132+
}
133+
134+
return $node;
135+
}
136+
137+
private function ensureAsCommandAttribute(Class_ $class): Attribute
138+
{
139+
foreach ($class->attrGroups as $attrGroup) {
140+
foreach ($attrGroup->attrs as $attribute) {
141+
if ($this->nodeNameResolver->isName($attribute->name, SymfonyAttribute::AS_COMMAND)) {
142+
return $attribute;
143+
}
144+
}
145+
}
146+
147+
$attrGroup = $this->phpAttributeGroupFactory->createFromClass(SymfonyAttribute::AS_COMMAND);
148+
$class->attrGroups[] = $attrGroup;
149+
150+
return $attrGroup->attrs[0];
151+
}
152+
153+
/**
154+
* Returns the argument passed to setHelp() and removes the MethodCall node.
155+
*/
156+
private function findAndRemoveSetHelpExpr(ClassMethod $configureMethod): ?String_
157+
{
158+
$helpString = null;
159+
160+
$this->traverseNodesWithCallable(
161+
(array) $configureMethod->stmts,
162+
function (Node $node) use (&$helpString): ?Expr {
163+
if (! $node instanceof MethodCall) {
164+
return null;
165+
}
166+
167+
if (! $this->isName($node->name, 'setHelp')) {
168+
return null;
169+
}
170+
171+
$argExpr = $node->getArgs()[0]
172+
->value;
173+
if ($argExpr instanceof String_) {
174+
$helpString = $argExpr;
175+
}
176+
177+
$parent = $node->getAttribute('parent');
178+
if ($parent instanceof Expression) {
179+
unset($parent);
180+
}
181+
182+
return $node->var;
183+
}
184+
);
185+
186+
foreach ((array) $configureMethod->stmts as $key => $stmt) {
187+
if ($this->isExpressionVariableThis($stmt)) {
188+
unset($configureMethod->stmts[$key]);
189+
}
190+
}
191+
192+
return $helpString;
193+
}
194+
195+
private function isExpressionVariableThis(Stmt $stmt): bool
196+
{
197+
if (! $stmt instanceof Expression) {
198+
return false;
199+
}
200+
201+
if (! $stmt->expr instanceof Variable) {
202+
return false;
203+
}
204+
205+
return $this->isName($stmt->expr, 'this');
206+
}
207+
}

0 commit comments

Comments
 (0)