Skip to content

Commit b181e83

Browse files
committed
Rule for checking of usage of obsolete parent/next/previous node attributes
1 parent d97bc43 commit b181e83

8 files changed

+203
-0
lines changed

conf/bleedingEdge.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ parameters:
77
arrayFilter: true
88
arrayUnpacking: true
99
nodeConnectingVisitorCompatibility: false
10+
nodeConnectingVisitorRule: true
1011
disableRuntimeReflectionProvider: true
1112
illegalConstructorMethodCall: true

conf/config.level0.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ parameters:
22
customRulesetUsed: false
33

44
conditionalTags:
5+
PHPStan\Rules\Api\NodeConnectingVisitorAttributesRule:
6+
phpstan.rules.rule: %featureToggles.nodeConnectingVisitorRule%
57
PHPStan\Rules\Properties\UninitializedPropertyRule:
68
phpstan.rules.rule: %checkUninitializedProperties%
79

@@ -67,6 +69,8 @@ rules:
6769
- PHPStan\Rules\Whitespace\FileWhitespaceRule
6870

6971
services:
72+
-
73+
class: PHPStan\Rules\Api\NodeConnectingVisitorAttributesRule
7074
-
7175
class: PHPStan\Rules\Classes\ExistingClassInClassExtendsRule
7276
tags:

conf/config.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ parameters:
3232
arrayFilter: false
3333
arrayUnpacking: false
3434
nodeConnectingVisitorCompatibility: true
35+
nodeConnectingVisitorRule: false
3536
illegalConstructorMethodCall: false
3637
fileExtensions:
3738
- php
@@ -220,6 +221,7 @@ parametersSchema:
220221
arrayFilter: bool(),
221222
arrayUnpacking: bool(),
222223
nodeConnectingVisitorCompatibility: bool(),
224+
nodeConnectingVisitorRule: bool(),
223225
illegalConstructorMethodCall: bool(),
224226
])
225227
fileExtensions: listOf(string())
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Api;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PhpParser\NodeVisitor\NodeConnectingVisitor;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\DependencyInjection\Container;
10+
use PHPStan\Parser\RichParser;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use PHPStan\Type\Constant\ConstantStringType;
14+
use PHPStan\Type\ObjectType;
15+
use function array_keys;
16+
use function in_array;
17+
use function sprintf;
18+
use function strpos;
19+
20+
/**
21+
* @implements Rule<MethodCall>
22+
*/
23+
class NodeConnectingVisitorAttributesRule implements Rule
24+
{
25+
26+
public function __construct(private Container $container)
27+
{
28+
}
29+
30+
public function getNodeType(): string
31+
{
32+
return MethodCall::class;
33+
}
34+
35+
public function processNode(Node $node, Scope $scope): array
36+
{
37+
if (!$node->name instanceof Node\Identifier) {
38+
return [];
39+
}
40+
if ($node->name->toLowerString() !== 'getattribute') {
41+
return [];
42+
}
43+
$calledOnType = $scope->getType($node->var);
44+
if (!(new ObjectType(Node::class))->isSuperTypeOf($calledOnType)->yes()) {
45+
return [];
46+
}
47+
$args = $node->getArgs();
48+
if (!isset($args[0])) {
49+
return [];
50+
}
51+
$argType = $scope->getType($args[0]->value);
52+
if (!$argType instanceof ConstantStringType) {
53+
return [];
54+
}
55+
if (!in_array($argType->getValue(), ['parent', 'previous', 'next'], true)) {
56+
return [];
57+
}
58+
if (!$scope->isInClass()) {
59+
return [];
60+
}
61+
62+
$classReflection = $scope->getClassReflection();
63+
$hasPhpStanInterface = false;
64+
foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) {
65+
if (strpos($interfaceName, 'PHPStan\\') !== 0) {
66+
continue;
67+
}
68+
69+
$hasPhpStanInterface = true;
70+
}
71+
72+
if (!$hasPhpStanInterface) {
73+
return [];
74+
}
75+
76+
$isVisitorRegistered = false;
77+
foreach ($this->container->getServicesByTag(RichParser::VISITOR_SERVICE_TAG) as $service) {
78+
if ($service::class !== NodeConnectingVisitor::class) {
79+
continue;
80+
}
81+
82+
$isVisitorRegistered = true;
83+
break;
84+
}
85+
86+
if ($isVisitorRegistered) {
87+
return [];
88+
}
89+
90+
return [
91+
RuleErrorBuilder::message(sprintf('Node attribute \'%s\' is no longer available.', $argType->getValue()))
92+
->tip('See: https://phpstan.org/blog/preprocessing-ast-for-custom-rules')
93+
->build(),
94+
];
95+
}
96+
97+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Api;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<NodeConnectingVisitorAttributesRule>
10+
*/
11+
class NodeConnectingVisitorAttributesRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new NodeConnectingVisitorAttributesRule(self::getContainer());
17+
}
18+
19+
public function testRule(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/node-connecting-visitor.php'], [
22+
[
23+
'Node attribute \'parent\' is no longer available.',
24+
18,
25+
'See: https://phpstan.org/blog/preprocessing-ast-for-custom-rules'
26+
],
27+
]);
28+
}
29+
30+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Api;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use function array_merge;
8+
9+
/**
10+
* @extends RuleTestCase<NodeConnectingVisitorAttributesRule>
11+
*/
12+
class NodeConnectingVisitorAttributesRuleWithVisitorRegisteredTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new NodeConnectingVisitorAttributesRule(self::getContainer());
18+
}
19+
20+
public function testRule(): void
21+
{
22+
$this->analyse([__DIR__ . '/data/node-connecting-visitor.php'], []);
23+
}
24+
25+
public static function getAdditionalConfigFiles(): array
26+
{
27+
return array_merge(parent::getAdditionalConfigFiles(), [
28+
__DIR__ . '/nodeConnectingVisitorCompatibility.neon',
29+
]);
30+
}
31+
32+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace NodeConnectingVisitorRule;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Rules\Rule;
8+
9+
class MyRule implements Rule
10+
{
11+
public function getNodeType(): string
12+
{
13+
return Node::class;
14+
}
15+
16+
public function processNode(Node $node, Scope $scope): array
17+
{
18+
$parent = $node->getAttribute("parent");
19+
$custom = $node->getAttribute("myCustomAttribute");
20+
21+
return [];
22+
}
23+
24+
}
25+
26+
class Foo
27+
{
28+
29+
public function doFoo(Node $node): void
30+
{
31+
$parent = $node->getAttribute("parent");
32+
}
33+
34+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
conditionalTags:
2+
PhpParser\NodeVisitor\NodeConnectingVisitor:
3+
phpstan.parser.richParserNodeVisitor: true

0 commit comments

Comments
 (0)