Skip to content

Commit 2d00ab3

Browse files
committed
[code-quality] Add SecurityAttributeToIsGrantedRector
1 parent 19521bd commit 2d00ab3

File tree

9 files changed

+203
-25
lines changed

9 files changed

+203
-25
lines changed

config/sets/symfony/symfony-code-quality.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
declare(strict_types=1);
44

5-
use Rector\Symfony\Symfony62\Rector\Class_\SecurityAttributeToIsGrantedAttributeRector;
65
use Rector\Config\RectorConfig;
6+
use Rector\Symfony\CodeQuality\Rector\AttributeGroup\SingleConditionSecurityAttributeToIsGrantedRector;
77
use Rector\Symfony\CodeQuality\Rector\BinaryOp\RequestIsMainRector;
88
use Rector\Symfony\CodeQuality\Rector\BinaryOp\ResponseStatusCodeRector;
99
use Rector\Symfony\CodeQuality\Rector\Class_\EventListenerToEventSubscriberRector;
@@ -16,6 +16,7 @@
1616
use Rector\Symfony\CodeQuality\Rector\MethodCall\AssertSameResponseCodeWithDebugContentsRector;
1717
use Rector\Symfony\CodeQuality\Rector\MethodCall\LiteralGetToRequestClassConstantRector;
1818
use Rector\Symfony\Symfony26\Rector\MethodCall\RedirectToRouteRector;
19+
use Rector\Symfony\Symfony62\Rector\Class_\SecurityAttributeToIsGrantedAttributeRector;
1920

2021
return static function (RectorConfig $rectorConfig): void {
2122
$rectorConfig->rules([
@@ -41,6 +42,6 @@
4142
InlineClassRoutePrefixRector::class,
4243

4344
// narrow attributes
44-
SecurityAttributeToIsGrantedAttributeRector::class,
45+
SingleConditionSecurityAttributeToIsGrantedRector::class,
4546
]);
4647
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Rector\Symfony\Tests\CodeQuality\Rector\AttributeGroup\SingleConditionSecurityAttributeToIsGrantedRector\Fixture;
4+
5+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
6+
7+
final class ControllerWithSecurityAttribute
8+
{
9+
#[Security("has_role('SOME_USER')")]
10+
public function index()
11+
{
12+
}
13+
}
14+
-----
15+
<?php
16+
17+
namespace Rector\Symfony\Tests\CodeQuality\Rector\AttributeGroup\SingleConditionSecurityAttributeToIsGrantedRector\Fixture;
18+
19+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
20+
21+
final class ControllerWithSecurityAttribute
22+
{
23+
#[\Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted('SOME_USER')]
24+
public function index()
25+
{
26+
}
27+
}
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\CodeQuality\Rector\AttributeGroup\SingleConditionSecurityAttributeToIsGrantedRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class SingleConditionSecurityAttributeToIsGrantedRectorTest 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+
}
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\CodeQuality\Rector\AttributeGroup\SingleConditionSecurityAttributeToIsGrantedRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(SingleConditionSecurityAttributeToIsGrantedRector::class);
10+
};
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\CodeQuality\NodeAnalyzer;
6+
7+
use PHPStan\Reflection\ReflectionProvider;
8+
9+
final readonly class AttributePresenceDetector
10+
{
11+
public function __construct(
12+
private ReflectionProvider $reflectionProvider,
13+
) {
14+
}
15+
16+
public function detect(string $attributeClass): bool
17+
{
18+
// run only if the sensio attribute is available
19+
if (! $this->reflectionProvider->hasClass($attributeClass)) {
20+
return false;
21+
}
22+
23+
// must be attribute, not just annotation
24+
$securityClassReflection = $this->reflectionProvider->getClass($attributeClass);
25+
26+
return $securityClassReflection->isAttributeClass();
27+
}
28+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\CodeQuality\Rector\AttributeGroup;
6+
7+
use Nette\Utils\Strings;
8+
use PhpParser\Node;
9+
use PhpParser\Node\Arg;
10+
use PhpParser\Node\AttributeGroup;
11+
use PhpParser\Node\Name\FullyQualified;
12+
use PhpParser\Node\Scalar\String_;
13+
use Rector\Rector\AbstractRector;
14+
use Rector\Symfony\CodeQuality\NodeAnalyzer\AttributePresenceDetector;
15+
use Rector\Symfony\Enum\SensioAttribute;
16+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
17+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
18+
19+
final class SingleConditionSecurityAttributeToIsGrantedRector extends AbstractRector
20+
{
21+
public function __construct(
22+
private readonly AttributePresenceDetector $attributePresenceDetector,
23+
) {
24+
}
25+
26+
public function getRuleDefinition(): RuleDefinition
27+
{
28+
return new RuleDefinition(
29+
'Narrow #[Security] attribute with inner sigle "is_granted/has_role" condition string to #[IsGranted] attribute',
30+
[
31+
new CodeSample(
32+
<<<'CODE_SAMPLE'
33+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
34+
35+
#[Security("is_granted('ROLE_USER')")]
36+
class SomeClass
37+
{
38+
}
39+
CODE_SAMPLE
40+
,
41+
<<<'CODE_SAMPLE'
42+
use Symfony\Component\Security\Http\Attribute\IsGranted;
43+
44+
#[IsGranted('ROLE_USER')]
45+
class SomeClass
46+
{
47+
}
48+
CODE_SAMPLE
49+
,
50+
),
51+
52+
]
53+
);
54+
}
55+
56+
public function getNodeTypes(): array
57+
{
58+
return [AttributeGroup::class];
59+
}
60+
61+
/**
62+
* @param AttributeGroup $node
63+
*/
64+
public function refactor(Node $node): ?AttributeGroup
65+
{
66+
if (! $this->attributePresenceDetector->detect(SensioAttribute::SECURITY)) {
67+
return null;
68+
}
69+
70+
foreach ($node->attrs as $attr) {
71+
if (! $this->isName($attr->name, SensioAttribute::SECURITY)) {
72+
continue;
73+
}
74+
75+
$firstArgValue = $attr->args[0]->value;
76+
if (! $firstArgValue instanceof String_) {
77+
continue;
78+
}
79+
80+
$matches = Strings::match(
81+
$firstArgValue->value,
82+
'#^(is_granted|has_role)\(\'(?<access_right>[A-Za-z_]+)\'\)$#'
83+
);
84+
if (! isset($matches['access_right'])) {
85+
continue;
86+
}
87+
88+
$attr->name = new FullyQualified(SensioAttribute::IS_GRANTED);
89+
$attr->args = [new Arg(new String_($matches['access_right']))];
90+
91+
return $node;
92+
}
93+
94+
return null;
95+
}
96+
}

rules/CodeQuality/Rector/Class_/EventListenerToEventSubscriberRector.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,7 @@ private function hasAsListenerAttribute(Class_ $class): bool
179179
continue;
180180
}
181181

182-
if ($this->phpAttributeAnalyzer->hasPhpAttribute(
183-
$classMethod,
184-
SymfonyAttribute::AS_EVENT_LISTENER
185-
)) {
182+
if ($this->phpAttributeAnalyzer->hasPhpAttribute($classMethod, SymfonyAttribute::AS_EVENT_LISTENER)) {
186183
return true;
187184
}
188185
}

rules/Symfony62/Rector/Class_/SecurityAttributeToIsGrantedAttributeRector.php

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PhpParser\Node\Stmt\ClassMethod;
1717
use PHPStan\Reflection\ReflectionProvider;
1818
use Rector\Rector\AbstractRector;
19+
use Rector\Symfony\CodeQuality\NodeAnalyzer\AttributePresenceDetector;
1920
use Rector\Symfony\Enum\SensioAttribute;
2021
use Rector\Symfony\Enum\SymfonyAttribute;
2122
use Rector\ValueObject\PhpVersionFeature;
@@ -31,7 +32,6 @@
3132
*/
3233
final class SecurityAttributeToIsGrantedAttributeRector extends AbstractRector implements MinPhpVersionInterface
3334
{
34-
3535
/**
3636
* @var string
3737
* @see https://regex101.com/r/Si1sDz/1
@@ -45,7 +45,8 @@ final class SecurityAttributeToIsGrantedAttributeRector extends AbstractRector i
4545
private const IS_GRANTED_AND_SUBJECT_REGEX = '#^is_granted\((\"|\')(?<role>[\w]+)(\"|\'),\s+(?<subject>\w+)\)$#';
4646

4747
public function __construct(
48-
private readonly ReflectionProvider $reflectionProvider
48+
private readonly ReflectionProvider $reflectionProvider,
49+
private readonly AttributePresenceDetector $attributePresenceDetector,
4950
) {
5051
}
5152

@@ -83,12 +84,12 @@ public function list()
8384
8485
class PostController extends Controller
8586
{
86-
#[IsGranted('ROLE_ADMIN')]
87+
#[IsGranted(attribute: 'ROLE_ADMIN')]
8788
public function index()
8889
{
8990
}
9091
91-
#[IsGranted(new Expression("is_granted('ROLE_ADMIN') and is_granted('ROLE_FRIENDLY_USER')"))]
92+
#[IsGranted(attribute: new Expression("is_granted('ROLE_ADMIN') and is_granted('ROLE_FRIENDLY_USER')"))]
9293
public function list()
9394
{
9495
}
@@ -112,7 +113,7 @@ public function getNodeTypes(): array
112113
*/
113114
public function refactor(Node $node): ?Node
114115
{
115-
if (! $this->hasSymfonySecurityAttribute()) {
116+
if (! $this->attributePresenceDetector->detect(SensioAttribute::SECURITY)) {
116117
return null;
117118
}
118119

@@ -126,7 +127,6 @@ public function refactor(Node $node): ?Node
126127

127128
// 1. resolve closest existing name of IsGranted
128129
$isGrantedName = $this->resolveIsGrantedAttributeName();
129-
130130
$attribute->name = new FullyQualified($isGrantedName);
131131

132132
$firstArg = $attribute->args[0];
@@ -192,19 +192,7 @@ private function resolveIsGrantedAttributeName(): string
192192
return SymfonyAttribute::IS_GRANTED;
193193
}
194194

195-
// fallback to sensio, if available
195+
// fallback to "sensio"
196196
return SensioAttribute::IS_GRANTED;
197197
}
198-
199-
private function hasSymfonySecurityAttribute(): bool
200-
{
201-
// run only if the sensio attribute is available
202-
if (! $this->reflectionProvider->hasClass(SensioAttribute::SECURITY)) {
203-
return false;
204-
}
205-
206-
// must be attribute, not just annotation
207-
$securityClassReflection = $this->reflectionProvider->getClass(SensioAttribute::SECURITY);
208-
return $securityClassReflection->isAttributeClass();
209-
}
210198
}

stubs/Symfony/Bundle/FrameworkExtraBundle/Configuration/Security.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@
55
#[\Attribute]
66
class Security
77
{
8+
public function __construct(string $expression)
9+
{
10+
}
811
}

0 commit comments

Comments
 (0)