8000 Add attribute support to TemplateAnnotationToThisRenderRector (#754) · JohJohan/rector-symfony@fdcb6e2 · GitHub
[go: up one dir, main page]

8000
Skip to content

Commit fdcb6e2

Browse files
authored
Add attribute support to TemplateAnnotationToThisRenderRector (rectorphp#754)
1 parent b719ac7 commit fdcb6e2

File tree

5 files changed

+189
-84
lines changed

5 files changed

+189
-84
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace AppBundle\Controller\Attributes;
4+
5+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
6+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
7+
8+
final class ClassWithNestedArrayController extends AbstractController
9+
{
10+
#[Template("with_some_template.twig")]
11+
public function indexAction()
12+
{
13+
return [
14+
'hello' => 'world'
15+
];
16+
}
17+
}
18+
19+
?>
20+
-----
21+
<?php
22+
23+
namespace AppBundle\Controller\Attributes;
24+
25+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
26+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
27+
28+
final class ClassWithNestedArrayController extends AbstractController
29+
{
30+
public function indexAction(): \Symfony\Component\HttpFoundation\Response
31+
{
32+
return $this->render('with_some_template.twig', [
33+
'hello' => 'world'
34+
]);
35+
}
36+
}
37+
38+
?>

rules/CodeQuality/Rector/ClassMethod/TemplateAnnotationToThisRenderRector.php

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Rector\Symfony\CodeQuality\Rector\ClassMethod;
66

77
use PhpParser\Node;
8+
use PhpParser\Node\Attribute;
89
use PhpParser\Node\Expr;
910
use PhpParser\Node\Expr\Closure;
1011
use PhpParser\Node\Expr\MethodCall;
@@ -24,6 +25,7 @@
2425
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover;
2526
use Rector\Comments\NodeDocBlock\DocBlockUpdater;
2627
use Rector\Contract\PhpParser\Node\StmtsAwareInterface;
28+
use Rector\Doctrine\NodeAnalyzer\AttributeFinder;
2729
use Rector\PhpParser\Node\BetterNodeFinder;
2830
use Rector\Rector\AbstractRector;
2931
use Rector\Symfony\Annotation\AnnotationAnalyzer;
@@ -37,8 +39,8 @@
3739
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
3840

3941
/**
40-
* @changelog https://github.com/symfony/symfony-docs/pull/12387#discussion_r329551967
41-
* @changelog https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/view.html
42+
* @see https://github.com/symfony/symfony-docs/pull/12387#discussion_r329551967
43+
* @see https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/view.html
4244
*
4345
* @see \Rector\Symfony\Tests\CodeQuality\Rector\ClassMethod\TemplateAnnotationToThisRenderRector\TemplateAnnotationToThisRenderRectorTest
4446
*/
@@ -54,6 +56,7 @@ public function __construct(
5456
private readonly DocBlockUpdater $docBlockUpdater,
5557
private readonly BetterNodeFinder $betterNodeFinder,
5658
private readonly PhpDocInfoFactory $phpDocInfoFactory,
59+
private readonly AttributeFinder $attributeFinder,
5760
) {
5861
}
5962

@@ -119,15 +122,14 @@ public function refactor(Node $node): ?Node
119122
SymfonyAnnotation::TEMPLATE
120123
);
121124

122< 8000 span class="diff-text-marker">-
foreach ($node->getMethods() as $classMethod) {
123-
if (! $classMethod->isPublic()) {
124-
continue;
125-
}
125+
$classTemplateAttribute = $this->attributeFinder->findAttributeByClass($node, SymfonyAnnotation::TEMPLATE);
126126

127+
foreach ($node->getMethods() as $classMethod) {
127128
$hasClassMethodChanged = $this->replaceTemplateAnnotation(
128129
$classMethod,
129-
$classDoctrineAnnotationTagValueNode
130+
$classDoctrineAnnotationTagValueNode ?: $classTemplateAttribute,
130131
);
132+
131133
if ($hasClassMethodChanged) {
132134
$hasChanged = true;
133135
}
@@ -137,7 +139,7 @@ public function refactor(Node $node): ?Node
137139
return null;
138140
}
139141

140-
// cleanup Class_ @Template annotaion
142+
// cleanup Class_ @Template annotation
141143
if ($classDoctrineAnnotationTagValueNode instanceof DoctrineAnnotationTagValueNode) {
142144
$this->removeDoctrineAnnotationTagValueNode($node, $classDoctrineAnnotationTagValueNode);
143145
}
@@ -157,7 +159,7 @@ private function decorateAbstractControllerParentClass(Class_ $class): void
157159

158160
private function replaceTemplateAnnotation(
159161
ClassMethod $classMethod,
160-
?DoctrineAnnotationTagValueNode $classDoctrineAnnotationTagValueNode
162+
DoctrineAnnotationTagValueNode | Attribute | null $classTagValueNodeOrAttribute
161163
): bool {
162164
if (! $classMethod->isPublic()) {
163165
return false;
@@ -168,28 +170,30 @@ private function replaceTemplateAnnotation(
168170
SymfonyAnnotation::TEMPLATE
169171
);
170172

171-
if ($doctrineAnnotationTagValueNode instanceof DoctrineAnnotationTagValueNode) {
172-
return $this->refactorClassMethod($classMethod, $doctrineAnnotationTagValueNode);
173+
$templateAttribute = $this->attributeFinder->findAttributeByClass($classMethod, SymfonyAnnotation::TEMPLATE);
174+
175+
if ($doctrineAnnotationTagValueNode instanceof DoctrineAnnotationTagValueNode || $templateAttribute instanceof Attribute) {
176+
return $this->refactorClassMethod($classMethod, $doctrineAnnotationTagValueNode ?: $templateAttribute);
173177
}
174178

12AE
175179
// global @Template access
176-
if ($classDoctrineAnnotationTagValueNode instanceof DoctrineAnnotationTagValueNode) {
177-
return $this->refactorClassMethod($classMethod, $classDoctrineAnnotationTagValueNode);
180+
if ($classTagValueNodeOrAttribute instanceof DoctrineAnnotationTagValueNode || $classTagValueNodeOrAttribute instanceof Attribute) {
181+
return $this->refactorClassMethod($classMethod, $classTagValueNodeOrAttribute);
178182
}
179183

180184
return false;
181185
}
182186

183187
private function refactorClassMethod(
184188
ClassMethod $classMethod,
185-
DoctrineAnnotationTagValueNode $templateDoctrineAnnotationTagValueNode
189+
DoctrineAnnotationTagValueNode | Attribute $templateTagValueNodeOrAttribute,
186190
): bool {
187191
$hasThisRenderOrReturnsResponse = $this->hasLastReturnResponse($classMethod);
188192

189193
$hasChanged = false;
190194

191195
$this->traverseNodesWithCallable($classMethod, function (Node $node) use (
192-
$templateDoctrineAnnotationTagValueNode,
196+
$templateTagValueNodeOrAttribute,
193197
$hasThisRenderOrReturnsResponse,
194198
$classMethod,
195199
&$hasChanged
@@ -206,7 +210,7 @@ private function refactorClassMethod(
206210

207211
$hasChangedNode = $this->refactorStmtsAwareNode(
208212
$node,
209-
$templateDoctrineAnnotationTagValueNode,
213+
$templateTagValueNodeOrAttribute,
210214
$hasThisRenderOrReturnsResponse,
211215
$classMethod
212216
);
@@ -223,11 +227,11 @@ private function refactorClassMethod(
223227

224228
$thisRenderMethodCall = $this->thisRenderFactory->create(
225229
null,
226-
$templateDoctrineAnnotationTagValueNode,
230+
$templateTagValueNodeOrAttribute,
227231
$classMethod
228232
);
229233

230-
$this->refactorNoReturn($classMethod, $thisRenderMethodCall, $templateDoctrineAnnotationTagValueNode);
234+
$this->refactorNoReturn($classMethod, $thisRenderMethodCall, $templateTagValueNodeOrAttribute);
231235

232236
return true;
233237
}
@@ -254,7 +258,7 @@ private function hasLastReturnResponse(ClassMethod $classMethod): bool
254258

255259
private function refactorReturn(
256260
Return_ $return,
257-
DoctrineAnnotationTagValueNode $templateDoctrineAnnotationTagValueNode,
261+
DoctrineAnnotationTagValueNode | Attribute $templateTagValueNodeOrAttribute,
258262
bool $hasThisRenderOrReturnsResponse,
259263
ClassMethod $classMethod
260264
): bool {
@@ -266,7 +270,7 @@ private function refactorReturn(
266270
// create "$this->render('template.file.twig.html', ['key' => 'value']);" method call
267271
$thisRenderMethodCall = $this->thisRenderFactory->create(
268272
$return,
269-
$templateDoctrineAnnotationTagValueNode,
273+
$templateTagValueNodeOrAttribute,
270274
$classMethod
271275
);
272276

@@ -275,7 +279,7 @@ private function refactorReturn(
275279
$hasThisRenderOrReturnsResponse,
276280
$thisRenderMethodCall,
277281
$classMethod,
278-
$templateDoctrineAnnotationTagValueNode
282+
$templateTagValueNodeOrAttribute
279283
);
280284
}
281285

@@ -295,7 +299,7 @@ private function refactorReturnWithValue(
295299
bool $hasThisRenderOrReturnsResponse,
296300
MethodCall $thisRenderMethodCall,
297301
ClassMethod $classMethod,
298-
DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode
302+
DoctrineAnnotationTagValueNode | Attribute $doctrineTagValueNodeOrAttribute
299303
): bool {
300304
/** @var Expr $lastReturnExpr */
301305
$lastReturnExpr = $return->expr;
@@ -324,24 +328,41 @@ private function refactorReturnWithValue(
324328
}
325329

326330
// already response
327-
$this->removeDoctrineAnnotationTagValueNode($classMethod, $doctrineAnnotationTagValueNode);
331+
$this->removeDoctrineAnnotationTagValueNode($classMethod, $doctrineTagValueNodeOrAttribute);
328332
$this->returnTypeDeclarationUpdater->updateClassMethod($classMethod, SymfonyClass::RESPONSE);
329333
return true;
330334
}
331335

332336
private function removeDoctrineAnnotationTagValueNode(
333337
Class_|ClassMethod $node,
334-
DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode
338+
DoctrineAnnotationTagValueNode | Attribute $doctrineTagValueNodeOrAttribute
335339
): void {
336-
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
337-
$this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $doctrineAnnotationTagValueNode);
340+
if ($doctrineTagValueNodeOrAttribute instanceof DoctrineAnnotationTagValueNode) {
341+
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
342+
$this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $doctrineTagValueNodeOrAttribute);
343+
344+
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node);
345+
346+
return;
347+
}
338348

339-
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node);
349+
foreach ($node->attrGroups as $attrGroupKey => $attrGroup) {
350+
foreach ($attrGroup->attrs as $attributeKey => $attribute) {
351+
if ($attribute === $doctrineTagValueNodeOrAttribute) {
352+
unset($attrGroup->attrs[$attributeKey]);
353+
}
354+
}
355+
356+
// no attributes left? remove the whole dgroup
357+
if ($attrGroup->attrs === []) {
358+
unset($node->attrGroups[$attrGroupKey]);
359+
}
360+
}
340361
}
341362

342363
private function refactorStmtsAwareNode(
343364
StmtsAwareInterface $stmtsAware,
344-
DoctrineAnnotationTagValueNode $templateDoctrineAnnotationTagValueNode,
365+
DoctrineAnnotationTagValueNode | Attribute $templateTagValueNodeOrAttribute,
345366
bool $hasThisRenderOrReturnsResponse,
346367
ClassMethod $classMethod
347368
): bool {
@@ -362,7 +383,7 @@ private function refactorStmtsAwareNode(
362383

363384
$hasChangedReturn = $this->refactorReturn(
364385
$stmt,
365-
$templateDoctrineAnnotationTagValueNode,
386+
$templateTagValueNodeOrAttribute,
366387
$hasThisRenderOrReturnsResponse,
367388
$classMethod
368389
);

src/Annotation/AnnotationAnalyzer.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,25 @@
99
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
1010
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
1111
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
12+
use Rector\Doctrine\NodeAnalyzer\AttrinationFinder;
1213
use Rector\Symfony\Enum\SymfonyAnnotation;
1314

1415
final readonly class AnnotationAnalyzer
1516
{
1617
public function __construct(
1718
private PhpDocInfoFactory $phpDocInfoFactory,
19+
private AttrinationFinder $attrinationFinder
1820
) {
1921
}
2022

2123
public function hasClassMethodWithTemplateAnnotation(Class_ $class): bool
2224
{
23-
$classTemplateAnnotation = $this->getDoctrineAnnotationTagValueNode($class, SymfonyAnnotation::TEMPLATE);
24-
if ($classTemplateAnnotation instanceof DoctrineAnnotationTagValueNode) {
25+
if ($this->attrinationFinder->hasByOne($class, SymfonyAnnotation::TEMPLATE)) {
2526
return true;
2627
}
2728

2829
foreach ($class->getMethods() as $classMethod) {
29-
$classMethodTemplateAnnotation = $this->getDoctrineAnnotationTagValueNode(
30-
$classMethod,
31-
SymfonyAnnotation::TEMPLATE
32-
);
33-
34-
if ($classMethodTemplateAnnotation instanceof DoctrineAnnotationTagValueNode) {
30+
if ($this->attrinationFinder->hasByOne($classMethod, SymfonyAnnotation::TEMPLATE)) {
3531
return true;
3632
}
3733
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\NodeFactory\Annotations;
6+
7+
use PhpParser\Node\Arg;
8+
use PhpParser\Node\Attribute;
9+
use PhpParser\Node\Scalar\String_;
10+
use Rector\BetterPhpDocParser\PhpDoc\ArrayItemNode;
11+
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
12+
use Rector\BetterPhpDocParser\PhpDoc\StringNode;
13+
14+
final readonly class AnnotationOrAttributeValueResolver
15+
{
16+
public function resolve(
17+
DoctrineAnnotationTagValueNode | Attribute $tagValueNodeOrAttribute,
18+
string $desiredKey
19+
): mixed {
20+
if ($tagValueNodeOrAttribute instanceof DoctrineAnnotationTagValueNode) {
21+
$templateParameter = $tagValueNodeOrAttribute->getValue($desiredKey);
22+
if ($templateParameter instanceof ArrayItemNode) {
23+
$templateParameterValue = $templateParameter->value;
24+
25+
if ($templateParameterValue instanceof StringNode) {
26+
$templateParameterValue = $templateParameterValue->value;
27+
}
28+
29+
if (is_string($templateParameterValue)) {
30+
return $templateParameterValue;
31+
}
32+
}
33+
34+
$arrayItemNode = $tagValueNodeOrAttribute->getSilentValue();
35+
if ($arrayItemNode instanceof ArrayItemNode) {
36+
$arrayItemNodeValue = $arrayItemNode->value;
37+
38+
if ($arrayItemNodeValue instanceof StringNode) {
39+
$arrayItemNodeValue = $arrayItemNodeValue->value;
40+
}
41+
42+
if (is_string($arrayItemNodeValue)) {
43+
return $arrayItemNodeValue;
44+
}
45+
}
46+
47+
return null;
48+
}
49+
50+
foreach ($tagValueNodeOrAttribute->args as $attributeArg) {
51+
if (! $this->isKeyEmptyOrMatch($attributeArg, $desiredKey)) {
52+
continue;
53+
}
54+
55+
if (! $attributeArg->value instanceof String_) {
56+
continue;
57+
}
58+
59+
return $attributeArg->value->value;
60+
}
61+
62+
return null;
63+
}
64+
65+
private function isKeyEmptyOrMatch(Arg $attributeArg, string $desiredKey): bool
66+
{
67+
if ($attributeArg->name === null) {
68+
return true;
69+
}
70+
71+
return $attributeArg->name->toString() === $desiredKey;
72+
}
73+
}

0 commit comments

Comments
 (0)
0