Skip to content

Commit 3038467

Browse files
authored
[Symfony 73] add ConstraintOptionsToNamedArgumentsRector (#806)L
* [Symfony 73] add ConstraintOptionsToNamedArgumentsRector * fix Access to an undefined property PhpParser\Node\Arg|PhpParser\Node\VariadicPlaceholder::$value * fix namespace
1 parent c144f25 commit 3038467

File tree

7 files changed

+203
-0
lines changed

7 files changed

+203
-0
lines changed

config/sets/symfony/symfony7/symfony73.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@
1010
$rectorConfig->import(__DIR__ . '/symfony73/symfony73-console.php');
1111
$rectorConfig->import(__DIR__ . '/symfony73/symfony73-security-core.php');
1212
$rectorConfig->import(__DIR__ . '/symfony73/symfony73-twig-bundle.php');
13+
$rectorConfig->import(__DIR__ . '/symfony73/symfony73-validator.php');
1314
};
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_\ConstraintOptionsToNamedArgumentsRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rules([ConstraintOptionsToNamedArgumentsRector::class]);
10+
};
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_\ConstraintOptionsToNamedArgumentsRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class ConstraintOptionsToNamedArgumentsRectorTest 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: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\ConstraintOptionsToNamedArgumentsRector\Fixture;
4+
5+
use Symfony\Component\Form\AbstractType;
6+
use Symfony\Component\Form\Extension\Core\Type\TextType;
7+
use Symfony\Component\Form\FormBuilderInterface;
8+
use Symfony\Component\Validator\Constraints\NotBlank;
9+
10+
class SomeForm extends AbstractType
11+
{
12+
public function buildForm(FormBuilderInterface $builder, array $options): void
13+
{
14+
$builder
15+
->add('name', TextType::class, [
16+
'constraints' => [
17+
new NotBlank(['message' => 'Name is required.']),
18+
],
19+
]);
20+
}
21+
}
22+
?>
23+
-----
24+
<?php
25+
26+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\ConstraintOptionsToNamedArgumentsRector\Fixture;
27+
28+
use Symfony\Component\Form\AbstractType;
29+
use Symfony\Component\Form\Extension\Core\Type\TextType;
30+
use Symfony\Component\Form\FormBuilderInterface;
31+
use Symfony\Component\Validator\Constraints\NotBlank;
32+
33+
class SomeForm extends AbstractType
34+
{
35+
public function buildForm(FormBuilderInterface $builder, array $options): void
36+
{
37+
$builder
38+
->add('name', TextType::class, [
39+
'constraints' => [
40+
new NotBlank(message: 'Name is required.'),
41+
],
42+
]);
43+
}
44+
}
45+
?>
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_\ConstraintOptionsToNamedArgumentsRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(ConstraintOptionsToNamedArgumentsRector::class);
10+
};
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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\Expr\Array_;
10+
use PhpParser\Node\Expr\ArrayItem;
11+
use PhpParser\Node\Expr\New_;
12+
use PhpParser\Node\Name;
13+
use PhpParser\Node\Name\FullyQualified;
14+
use Rector\PhpParser\Node\Value\ValueResolver;
15+
use Rector\Rector\AbstractRector;
16+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
17+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
18+
19+
final class ConstraintOptionsToNamedArgumentsRector extends AbstractRector
20+
{
21+
public function __construct(
22+
private readonly ValueResolver $valueResolver,
23+
) {
24+
}
25+
26+
public function getRuleDefinition(): RuleDefinition
27+
{
28+
return new RuleDefinition(
29+
'Refactor Symfony constraints using array options to named arguments syntax for better readability and type safety.',
30+
[
31+
new CodeSample(
32+
<<<'CODE_SAMPLE'
33+
use Symfony\Component\Validator\Constraints\NotBlank;
34+
35+
$constraint = new NotBlank(['message' => 'This field should not be blank.']);
36+
CODE_SAMPLE
37+
,
38+
<<<'CODE_SAMPLE'
39+
use Symfony\Component\Validator\Constraints\NotBlank;
40+
41+
$constraint = new NotBlank(message: 'This field should not be blank.');
42+
CODE_SAMPLE
43+
)]
44+
);
45+
}
46+
47+
public function getNodeTypes(): array
48+
{
49+
return [New_::class];
50+
}
51+
52+
public function refactor(Node $node): ?Node
53+
{
54+
if (!$node instanceof New_) {
55+
return null;
56+
}
57+
58+
// Match classes starting with Symfony\Component\Validator\Constraints\
59+
if (!$node->class instanceof FullyQualified && !$node->class instanceof Name) {
60+
return null;
61+
}
62+
63+
$className = $this->getName($node->class);
64+
if (!is_string($className)) {
65+
return null;
66+
}
67+
68+
if (!str_starts_with($className, 'Symfony\Component\Validator\Constraints\\')) {
69+
return null;
70+
}
71+
72+
if (
73+
0 === count($node->args) ||
74+
!$node->args[0] instanceof Arg ||
75+
!$node->args[0]->value instanceof Array_
76+
) {
77+
return null;
78+
}
79+
80+
$array = $node->args[0]->value;
81+
$namedArgs = [];
82+
83+
foreach ($array->items as $item) {
84+
if (!$item instanceof ArrayItem || null === $item->key) {
85+
continue;
86+
}
87+
88+
$keyValue = $this->valueResolver->getValue($item->key);
89+
if (!is_string($keyValue)) {
90+
continue;
91+
}
92+
93+
$arg = new Node\Arg($item->value);
94+
$arg->name = new Node\Identifier($keyValue);
95+
96+
$namedArgs[] = $arg;
97+
}
98+
99+
$node->args = $namedArgs;
100+
101+
return $node;
102+
}
103+
}

src/Set/SetProvider/Symfony7SetProvider.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ public function provide(): array
104104
'7.3',
105105
__DIR__ . '/../../../config/sets/symfony/symfony7/symfony73/symfony73-twig-bundle.php'
106106
),
107+
new ComposerTriggeredSet(
108+
SetGroup::SYMFONY,
109+
'symfony/validator',
110+
'7.3',
111+
__DIR__ . '/../../../config/sets/symfony/symfony7/symfony73/symfony73-validator.php'
112+
),
107113
];
108114
}
109115
}

0 commit comments

Comments
 (0)