Skip to content

Commit 10b3c9e

Browse files
committed
[rector] add AvoidFeatureSetAttributeInRectorRule
1 parent 50a4095 commit 10b3c9e

File tree

8 files changed

+181
-1
lines changed

8 files changed

+181
-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: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
20+
/**
21+
* @see \Symplify\PHPStanRules\Tests\Rules\Rector\AvoidFeatureSetAttributeInRectorRule\AvoidFeatureSetAttributeInRectorRuleTest
22+
*
23+
* @implements Rule<Class_>
24+
*/
25+
final class AvoidFeatureSetAttributeInRectorRule implements Rule
26+
{
27+
/**
28+
* @var string
29+
*/
30+
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.';
31+
32+
/**
33+
* @var string[]
34+
*/
35+
private const ALLOWED_ATTRIBUTES = ['kind', 'origNode', 'comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos'];
36+
37+
public function getNodeType(): string
38+
{
39+
return InClassNode::class;
40+
}
41+
42+
/**
43+
* @param InClassNode $node
44+
*/
45+
public function processNode(Node $node, Scope $scope): array
46+
{
47+
$classReflection = $scope->getClassReflection();
48+
if (! $classReflection instanceof ClassReflection) {
49+
return [];
50+
}
51+
52+
if (! $classReflection->is(AbstractRector::class)) {
53+
return [];
54+
}
55+
56+
$classLike = $node->getOriginalNode();
57+
58+
$nodeFinder = new NodeFinder();
59+
60+
/** @var MethodCall[] $methodCalls */
61+
$methodCalls = $nodeFinder->findInstanceOf($classLike, MethodCall::class);
62+
63+
$ruleErrors = [];
64+
65+
foreach ($methodCalls as $methodCall) {
66+
if ($methodCall->name->toString() !== 'setAttribute') {
67+
continue;
68+
}
69+
70+
$firstArg = $methodCall->getArgs()[0];
71+
$attributeNameType = $scope->getType($firstArg->value);
72+
73+
if (! $attributeNameType instanceof ConstantStringType) {
74+
continue;
75+
}
76+
77+
$attributeName = $attributeNameType->getValue();
78+
if (in_array($attributeName, self::ALLOWED_ATTRIBUTES, true)) {
79+
continue;
80+
}
81+
82+
$ruleError = RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $attributeName))
83+
->identifier(RectorRuleIdentifier::AVOID_FEATURE_SET_ATTRIBUTE_IN_RECTOR)
84+
->build();
85+
86+
$ruleErrors[] = $ruleError;
87+
}
88+
89+
return $ruleErrors;
90+
}
91+
}
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)