Skip to content

Commit 8c94690

Browse files
Optimise checking the node types allowed for each rule (#6232)
* Optimise checking the node types allowed for each rule * Add tests for `getVisitorsForNode()` function * Make getVisitorsForNode function public * Move initialisation of `visitorsPerNodeClass` property * Use continue 2 instead of break * Use patch from vendor-patches instead of local patch
1 parent d97deb8 commit 8c94690

File tree

6 files changed

+230
-1
lines changed

6 files changed

+230
-1
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@
139139
"https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-if-php.patch",
140140
"https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-case-php.patch",
141141
"https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-elseif-php.patch",
142-
"https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-namespace-php.patch"
142+
"https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-namespace-php.patch",
143+
"https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-nodetraverser-php.patch"
143144
]
144145
},
145146
"composer-exit-on-patch-failure": true,

src/PhpParser/NodeTraverser/RectorNodeTraverser.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@
44

55
namespace Rector\PhpParser\NodeTraverser;
66

7+
use PhpParser\Node;
78
use PhpParser\Node\Stmt;
89
use PhpParser\NodeTraverser;
10+
use PhpParser\NodeVisitor;
911
use Rector\Contract\Rector\RectorInterface;
1012
use Rector\VersionBonding\PhpVersionedFilter;
1113

1214
final class RectorNodeTraverser extends NodeTraverser
1315
{
1416
private bool $areNodeVisitorsPrepared = false;
1517

18+
/**
19+
* @var array<class-string<Node>,RectorInterface[]>
20+
*/
21+
private array $visitorsPerNodeClass = [];
22+
1623
/**
1724
* @param RectorInterface[] $rectors
1825
*/
@@ -42,10 +49,36 @@ public function refreshPhpRectors(array $rectors): void
4249
{
4350
$this->rectors = $rectors;
4451
$this->visitors = [];
52+
$this->visitorsPerNodeClass = [];
4553

4654
$this->areNodeVisitorsPrepared = false;
4755
}
4856

57+
/**
58+
* We return the list of visitors (rector rules) that can be applied to each node class
59+
* This list is cached so that we don't need to continually check if a rule can be applied to a node
60+
*
61+
* @return NodeVisitor[]
62+
*/
63+
public function getVisitorsForNode(Node $node): array
64+
{
65+
$nodeClass = $node::class;
66+
if (! isset($this->visitorsPerNodeClass[$nodeClass])) {
67+
$this->visitorsPerNodeClass[$nodeClass] = [];
68+
foreach ($this->visitors as $visitor) {
69+
assert($visitor instanceof RectorInterface);
70+
foreach ($visitor->getNodeTypes() as $nodeType) {
71+
if (is_a($nodeClass, $nodeType, true)) {
72+
$this->visitorsPerNodeClass[$nodeClass][] = $visitor;
73+
continue 2;
74+
}
75+
}
76+
}
77+
}
78+
79+
return $this->visitorsPerNodeClass[$nodeClass];
80+
}
81+
4982
/**
5083
* This must happen after $this->configuration is set after ProcessCommand::execute() is run,
5184
* otherwise we get default false positives.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\PhpParser\NodeTraverser\ClassLike;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\ClassLike;
9+
use Rector\Rector\AbstractRector;
10+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
11+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
12+
13+
/**
14+
* @see \Rector\Tests\PhpParser\NodeTraverser\RectorNodeTraverserTest
15+
*/
16+
final class RuleUsingClassLikeRector extends AbstractRector
17+
{
18+
public function getRuleDefinition(): RuleDefinition
19+
{
20+
return new RuleDefinition('This rule applies to class like nodes', [new CodeSample('', '')]);
21+
}
22+
23+
/**
24+
* @return array<class-string<Node>>
25+
*/
26+
public function getNodeTypes(): array
27+
{
28+
return [ClassLike::class];
29+
}
30+
31+
public function refactor(Node $node): Node
32+
{
33+
return $node;
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\PhpParser\NodeTraverser\Class_;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\Class_;
9+
use Rector\Rector\AbstractRector;
10+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
11+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
12+
13+
/**
14+
* @see \Rector\Tests\PhpParser\NodeTraverser\RectorNodeTraverserTest
15+
*/
16+
final class RuleUsingClassRector extends AbstractRector
17+
{
18+
public function getRuleDefinition(): RuleDefinition
19+
{
20+
return new RuleDefinition('This rule applies to classes', [new CodeSample('', '')]);
21+
}
22+
23+
/**
24+
* @return array<class-string<Node>>
25+
*/
26+
public function getNodeTypes(): array
27+
{
28+
return [Class_::class];
29+
}
30+
31+
public function refactor(Node $node): Node
32+
{
33+
return $node;
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\PhpParser\NodeTraverser\Function_;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\Function_;
9+
use Rector\Rector\AbstractRector;
10+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
11+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
12+
13+
/**
14+
* @see \Rector\Tests\PhpParser\NodeTraverser\RectorNodeTraverserTest
15+
*/
16+
final class RuleUsingFunctionRector extends AbstractRector
17+
{
18+
public function getRuleDefinition(): RuleDefinition
19+
{
20+
return new RuleDefinition('This rule applies to functions', [new CodeSample('', '')]);
21+
}
22+
23+
/**
24+
* @return array<class-string<Node>>
25+
*/
26+
public function getNodeTypes(): array
27+
{
28+
return [Function_::class];
29+
}
30+
31+
public function refactor(Node $node): Node
32+
{
33+
return $node;
34+
}
35+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\PhpParser\NodeTraverser;
6+
7+
use PhpParser\Node\Stmt\Class_;
8+
use Rector\PhpParser\NodeTraverser\RectorNodeTraverser;
9+
use Rector\Testing\PHPUnit\AbstractLazyTestCase;
10+
use Rector\Tests\PhpParser\NodeTraverser\Class_\RuleUsingClassRector;
11+
use Rector\Tests\PhpParser\NodeTraverser\ClassLike\RuleUsingClassLikeRector;
12+
use Rector\Tests\PhpParser\NodeTraverser\Function_\RuleUsingFunctionRector;
13+
14+
final class RectorNodeTraverserTest extends AbstractLazyTestCase
15+
{
16+
private RectorNodeTraverser $rectorNodeTraverser;
17+
18+
private RuleUsingFunctionRector $ruleUsingFunctionRector;
19+
20+
private RuleUsingClassRector $ruleUsingClassRector;
21+
22+
private RuleUsingClassLikeRector $ruleUsingClassLikeRector;
23+
24+
protected function setUp(): void
25+
{
26+
$this->rectorNodeTraverser = $this->make(RectorNodeTraverser::class);
27+
$this->rectorNodeTraverser->refreshPhpRectors([]);
28+
29+
$this->ruleUsingFunctionRector = new RuleUsingFunctionRector();
30+
$this->ruleUsingClassRector = new RuleUsingClassRector();
31+
$this->ruleUsingClassLikeRector = new RuleUsingClassLikeRector();
32+
}
33+
34+
public function testGetVisitorsForNodeWhenNoVisitorsAvailable(): void
35+
{
36+
$class = new Class_('test');
37+
38+
$visitors = $this->rectorNodeTraverser->getVisitorsForNode($class);
39+
40+
$this->assertSame([], $visitors);
41+
}
42+
43+
public function testGetVisitorsForNodeWhenNoVisitorsMatch(): void
44+
{
45+
$class = new Class_('test');
46+
$this->rectorNodeTraverser->addVisitor($this->ruleUsingFunctionRector);
47+
48+
$visitors = $this->rectorNodeTraverser->getVisitorsForNode($class);
49+
50+
$this->assertSame([], $visitors);
51+
}
52+
53+
public function testGetVisitorsForNodeWhenSomeVisitorsMatch(): void
54+
{
55+
$class = new Class_('test');
56+
$this->rectorNodeTraverser->addVisitor($this->ruleUsingFunctionRector);
57+
$this->rectorNodeTraverser->addVisitor($this->ruleUsingClassRector);
58+
59+
$visitors = $this->rectorNodeTraverser->getVisitorsForNode($class);
60+
61+
$this->assertEquals([$this->ruleUsingClassRector], $visitors);
62+
}
63+
64+
public function testGetVisitorsForNodeWhenAllVisitorsMatch(): void
65+
{
66+
$class = new Class_('test');
67+
$this->rectorNodeTraverser->addVisitor($this->ruleUsingClassRector);
68+
$this->rectorNodeTraverser->addVisitor($this->ruleUsingClassLikeRector);
69+
70+
$visitors = $this->rectorNodeTraverser->getVisitorsForNode($class);
71+
72+
$this->assertEquals([$this->ruleUsingClassRector, $this->ruleUsingClassLikeRector], $visitors);
73+
}
74+
75+
public function testGetVisitorsForNodeUsesCachedValue(): void
76+
{
77+
$class = new Class_('test');
78+
$this->rectorNodeTraverser->addVisitor($this->ruleUsingClassRector);
79+
$this->rectorNodeTraverser->addVisitor($this->ruleUsingClassLikeRector);
80+
81+
$visitors = $this->rectorNodeTraverser->getVisitorsForNode($class);
82+
83+
$this->assertEquals([$this->ruleUsingClassRector, $this->ruleUsingClassLikeRector], $visitors);
84+
85+
$this->rectorNodeTraverser->removeVisitor($this->ruleUsingClassRector);
86+
$visitors = $this->rectorNodeTraverser->getVisitorsForNode($class);
87+
88+
$this->assertEquals([$this->ruleUsingClassRector, $this->ruleUsingClassLikeRector], $visitors);
89+
}
90+
}

0 commit comments

Comments
 (0)