Skip to content

Commit 1cc3801

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

File tree

12 files changed

+548
-2
lines changed

12 files changed

+548
-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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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
27+
Some help text
28+
TXT')]
29+
final class SomeCommand extends Command
30+
{
31+
public function configure()
32+
{
33+
}
34+
}
35+
36+
?>
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: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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
72+
Some help text
73+
TXT')]
74+
final class SomeCommand extends Command
75+
{
76+
}
77+
CODE_SAMPLE
78+
),
79+
]
80+
);
81+
}
82+
83+
/**
84+
* @return array<class-string<Node>>
85+
*/
86+
public function getNodeTypes(): array
87+
{
88+
return [Class_::class];
89+
}
90+
91+
/**
92+
* @param Class_ $node
93+
*/
94+
public function refactor(Node $node): ?Node
95+
{
96+
if ($node->isAbstract()) {
97+
return null;
98+
}
99+
100+
if (! $this->reflectionProvider->hasClass(SymfonyAttribute::AS_COMMAND)) {
101+
return null;
102+
}
103+
104+
if (! $this->isObjectType($node, new ObjectType(SymfonyClass::COMMAND))) {
105+
return null;
106+
}
107+
108+
$asCommandAttribute = $this->ensureAsCommandAttribute($node);
109+
110+
foreach ($asCommandAttribute->args as $arg) {
111+
if ($arg->name?->toString() === 'help') {
112+
return $node;
113+
}
114+
}
115+
116+
$configureClassMethod = $node->getMethod(CommandMethodName::CONFIGURE);
117+
if (! $configureClassMethod instanceof ClassMethod) {
118+
return null;
119+
}
120+
121+
$helpExpr = $this->findAndRemoveSetHelpExpr($configureClassMethod);
122+
if (! $helpExpr instanceof String_) {
123+
return null;
124+
}
125+
126+
$wrappedHelp = new String_(
127+
"<<<TXT\n" . $helpExpr->value . "\nTXT"
128+
);
129+
130+
$asCommandAttribute->args[] = new Arg($wrappedHelp, false, false, [], new Identifier('help'));
131+
132+
if ($configureClassMethod->stmts === []) {
133+
unset($configureClassMethod);
134+
}
135+
136+
return $node;
137+
}
138+
139+
private function ensureAsCommandAttribute(Class_ $class): Attribute
140+
{
141+
foreach ($class->attrGroups as $attrGroup) {
142+
foreach ($attrGroup->attrs as $attribute) {
143+
if ($this->nodeNameResolver->isName($attribute->name, SymfonyAttribute::AS_COMMAND)) {
144+
return $attribute;
145+
}
146+
}
147+
}
148+
149+
$attrGroup = $this->phpAttributeGroupFactory->createFromClass(SymfonyAttribute::AS_COMMAND);
150+
$class->attrGroups[] = $attrGroup;
151+
152+
return $attrGroup->attrs[0];
153+
}
154+
155+
/**
156+
* Returns the argument passed to setHelp() and removes the MethodCall node.
157+
*/
158+
private function findAndRemoveSetHelpExpr(ClassMethod $configureMethod): ?String_
159+
{
160+
$helpString = null;
161+
162+
$this->traverseNodesWithCallable(
163+
(array) $configureMethod->stmts,
164+
function (Node $node) use (&$helpString): ?Expr {
165+
if (! $node instanceof MethodCall) {
166+
return null;
167+
}
168+
169+
if (! $this->isName($node->name, 'setHelp')) {
170+
return null;
171+
}
172+
173+
$argExpr = $node->getArgs()[0]
174+
->value;
175+
if ($argExpr instanceof String_) {
176+
$helpString = $argExpr;
177+
}
178+
179+
$parent = $node->getAttribute('parent');
180+
if ($parent instanceof Expression) {
181+
unset($parent);
182+
}
183+
184+
return $node->var;
185+
}
186+
);
187+
188+
foreach ((array) $configureMethod->stmts as $key => $stmt) {
189+
if ($this->isExpressionVariableThis($stmt)) {
190+
unset($configureMethod->stmts[$key]);
191+
}
192+
}
193+
194+
return $helpString;
195+
}
196+
197+
private function isExpressionVariableThis(Stmt $stmt): bool
198+
{
199+
if (! $stmt instanceof Expression) {
200+
return false;
201+
}
202+
203+
if (! $stmt->expr instanceof Variable) {
204+
return false;
205+
}
206+
207+
return $this->isName($stmt->expr, 'this');
208+
}
209+
}

0 commit comments

Comments
 (0)