Skip to content

Commit 0dde30d

Browse files
committed
[rector] add AvoidFeatureSetAttributeInRectorRule
1 parent 50a4095 commit 0dde30d

File tree

8 files changed

+191
-1
lines changed

8 files changed

+191
-1
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"symfony/framework-bundle": "6.1.*",
1717
"phpecs/phpecs": "^2.2",
1818
"tomasvotruba/class-leak": "^2.1",
19-
"rector/rector": "^2.2",
19+
"rector/rector": "^2.2.11",
2020
"phpstan/extension-installer": "^1.4",
2121
"symplify/phpstan-extensions": "^12.0",
2222
"tomasvotruba/unused-public": "^2.1",

config/rector-rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ rules:
88
- Symplify\PHPStanRules\Rules\Rector\PreferDirectIsNameRule
99
- Symplify\PHPStanRules\Rules\Rector\NoOnlyNullReturnInRefactorRule
1010
- Symplify\PHPStanRules\Rules\Rector\NoIntegerRefactorReturnRule
11+
- Symplify\PHPStanRules\Rules\Rector\AvoidFeatureSetAttributeInRectorRule
1112

1213
services:
1314
# $node->getAttribute($1) => Type|null by $1

src/Enum/RuleIdentifier/RectorRuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ final class RectorRuleIdentifier
2121
public const NO_ONLY_NULL_RETURN_IN_REFACTOR = 'rector.noOnlyNullReturnInRefactor';
2222

2323
public const NO_INTEGER_REFACTOR_RETURN = 'rector.noIntegerRefactorReturn';
24+
25+
public const AVOID_FEATURE_SET_ATTRIBUTE_IN_RECTOR = 'rector.avoidFeatureSetAttributeInRector';
2426
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Rector;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PhpParser\Node\Stmt\Class_;
10+
use PhpParser\NodeFinder;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Node\InClassNode;
13+
use PHPStan\Reflection\ClassReflection;
14+
use PHPStan\Rules\Rule;
15+
use PHPStan\Rules\RuleErrorBuilder;
16+
use PHPStan\Type\Constant\ConstantStringType;
17+
use Rector\Rector\AbstractRector;
18+
use Symplify\PHPStanRules\Enum\RuleIdentifier\RectorRuleIdentifier;
19+
use Symplify\PHPStanRules\Helper\NamingHelper;
20+
21+
/**
22+
* @see \Symplify\PHPStanRules\Tests\Rules\Rector\AvoidFeatureSetAttributeInRectorRule\AvoidFeatureSetAttributeInRectorRuleTest
23+
*
24+
* @implements Rule<Class_>
25+
*/
26+
final class AvoidFeatureSetAttributeInRectorRule implements Rule
27+
{
28+
/**
29+
* @var string
30+
*/
31+
public const ERROR_MESSAGE = 'Instead of using Rector rule to setAttribute("%s") to be used later, create a service extending "DecoratingNodeVisitorInterface". This ensures attribute decoration and node changes are in 2 separated steps.';
32+
33+
/**
34+
* @var string[]
35+
*/
36+
private const ALLOWED_ATTRIBUTES = ['kind', 'origNode', 'comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos'];
37+
38+
public function getNodeType(): string
39+
{
40+
return InClassNode::class;
41+
}
42+
43+
/**
44+
* @param InClassNode $node
45+
*/
46+
public function processNode(Node $node, Scope $scope): array
47+
{
48+
$classReflection = $scope->getClassReflection();
49+
if (! $classReflection instanceof ClassReflection) {
50+
return [];
51+
}
52+
53+
if (! $classReflection->is(AbstractRector::class)) {
54+
return [];
55+
}
56+
57+
$classLike = $node->getOriginalNode();
58+
59+
$nodeFinder = new NodeFinder();
60+
61+
/** @var MethodCall[] $methodCalls */
62+
$methodCalls = $nodeFinder->findInstanceOf($classLike, MethodCall::class);
63+
64+
$ruleErrors = [];
65+
66+
foreach ($methodCalls as $methodCall) {
67+
if (! NamingHelper::isName($methodCall->name, 'setAttribute')) {
68+
continue;
69+
}
70+
71+
$attributeName = $this->resolveAttributeKeyValue($methodCall, $scope);
72+
if (! is_string($attributeName)) {
73+
continue;
74+
}
75+
76+
if (in_array($attributeName, self::ALLOWED_ATTRIBUTES, true)) {
77+
continue;
78+
}
79+
80+
$ruleError = RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $attributeName))
81+
->identifier(RectorRuleIdentifier::AVOID_FEATURE_SET_ATTRIBUTE_IN_RECTOR)
82+
->build();
83+
84+
$ruleErrors[] = $ruleError;
85+
}
86+
87+
return $ruleErrors;
88+
}
89+
90+
private function resolveAttributeKeyValue(MethodCall $methodCall, Scope $scope): ?string
91+
{
92+
$firstArg = $methodCall->getArgs()[0];
93+
$attributeNameType = $scope->getType($firstArg->value);
94+
95+
if (! $attributeNameType instanceof ConstantStringType) {
96+
return null;
97+
}
98+
99+
return $attributeNameType->getValue();
100+
}
101+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Rector\AvoidFeatureSetAttributeInRectorRule;
6+
7+
use Iterator;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use Symplify\PHPStanRules\Rules\Rector\AvoidFeatureSetAttributeInRectorRule;
12+
13+
final class AvoidFeatureSetAttributeInRectorRuleTest extends RuleTestCase
14+
{
15+
#[DataProvider('provideData')]
16+
public function testRule(string $filePath, array $expectedErrorsWithLines): void
17+
{
18+
$this->analyse([$filePath], $expectedErrorsWithLines);
19+
}
20+
21+
public static function provideData(): Iterator
22+
{
23+
yield [__DIR__ . '/Fixture/SetLocalAttribute.php', [[
24+
sprintf(AvoidFeatureSetAttributeInRectorRule::ERROR_MESSAGE, 'some_attribute'), 11,
25+
]]];
26+
27+
yield [__DIR__ . '/Fixture/SkipAllowedSetAttributesNode.php', []];
28+
}
29+
30+
protected function getRule(): Rule
31+
{
32+
return new AvoidFeatureSetAttributeInRectorRule();
33+
}
34+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Rector\AvoidFeatureSetAttributeInRectorRule\Fixture;
6+
7+
use PhpParser\Node;
8+
use Rector\Rector\AbstractRector;
9+
use ReflectionClass;
10+
11+
final class SetLocalAttribute extends AbstractRector
12+
{
13+
public function getNodeTypes(): array
14+
{
15+
return [Node\Stmt\Class_::class];
16+
}
17+
18+
public function refactor(Node $node)
19+
{
20+
$node->setAttribute('some_attribute', 'some_value');
21+
22+
return null;
23+
}
24+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Rector\AvoidFeatureSetAttributeInRectorRule\Fixture;
6+
7+
use PhpParser\Node;
8+
use Rector\NodeTypeResolver\Node\AttributeKey;
9+
use Rector\Rector\AbstractRector;
10+
use ReflectionClass;
11+
12+
final class SkipAllowedSetAttributesNode extends AbstractRector
13+
{
14+
public function getNodeTypes(): array
15+
{
16+
return [Node\Stmt\Class_::class];
17+
}
18+
19+
public function refactor(Node $node)
20+
{
21+
$node->setAttribute(AttributeKey::ORIGINAL_NODE, null);
22+
$node->setAttribute(AttributeKey::KIND, 1);
23+
24+
return null;
25+
}
26+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
rules:
2+
- Symplify\PHPStanRules\Rules\Rector\AvoidFeatureSetAttributeInRectorRule

0 commit comments

Comments
 (0)