diff --git a/composer.json b/composer.json index 5142158fad4..5694f744f2d 100644 --- a/composer.json +++ b/composer.json @@ -139,7 +139,8 @@ "https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-if-php.patch", "https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-case-php.patch", "https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-elseif-php.patch", - "https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-namespace-php.patch" + "https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-namespace-php.patch", + "https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-nodetraverser-php.patch" ] }, "composer-exit-on-patch-failure": true, diff --git a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php index b4acb6dea99..e5fb6d1b25d 100644 --- a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php +++ b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php @@ -4,8 +4,10 @@ namespace Rector\PhpParser\NodeTraverser; +use PhpParser\Node; use PhpParser\Node\Stmt; use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor; use Rector\Contract\Rector\RectorInterface; use Rector\VersionBonding\PhpVersionedFilter; @@ -13,6 +15,11 @@ final class RectorNodeTraverser extends NodeTraverser { private bool $areNodeVisitorsPrepared = false; + /** + * @var array,RectorInterface[]> + */ + private array $visitorsPerNodeClass = []; + /** * @param RectorInterface[] $rectors */ @@ -42,10 +49,36 @@ public function refreshPhpRectors(array $rectors): void { $this->rectors = $rectors; $this->visitors = []; + $this->visitorsPerNodeClass = []; $this->areNodeVisitorsPrepared = false; } + /** + * We return the list of visitors (rector rules) that can be applied to each node class + * This list is cached so that we don't need to continually check if a rule can be applied to a node + * + * @return NodeVisitor[] + */ + public function getVisitorsForNode(Node $node): array + { + $nodeClass = $node::class; + if (! isset($this->visitorsPerNodeClass[$nodeClass])) { + $this->visitorsPerNodeClass[$nodeClass] = []; + foreach ($this->visitors as $visitor) { + assert($visitor instanceof RectorInterface); + foreach ($visitor->getNodeTypes() as $nodeType) { + if (is_a($nodeClass, $nodeType, true)) { + $this->visitorsPerNodeClass[$nodeClass][] = $visitor; + continue 2; + } + } + } + } + + return $this->visitorsPerNodeClass[$nodeClass]; + } + /** * This must happen after $this->configuration is set after ProcessCommand::execute() is run, * otherwise we get default false positives. diff --git a/tests/PhpParser/NodeTraverser/ClassLike/RuleUsingClassLikeRector.php b/tests/PhpParser/NodeTraverser/ClassLike/RuleUsingClassLikeRector.php new file mode 100644 index 00000000000..44361f9432d --- /dev/null +++ b/tests/PhpParser/NodeTraverser/ClassLike/RuleUsingClassLikeRector.php @@ -0,0 +1,35 @@ +> + */ + public function getNodeTypes(): array + { + return [ClassLike::class]; + } + + public function refactor(Node $node): Node + { + return $node; + } +} diff --git a/tests/PhpParser/NodeTraverser/Class_/RuleUsingClassRector.php b/tests/PhpParser/NodeTraverser/Class_/RuleUsingClassRector.php new file mode 100644 index 00000000000..02e8070f83f --- /dev/null +++ b/tests/PhpParser/NodeTraverser/Class_/RuleUsingClassRector.php @@ -0,0 +1,35 @@ +> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + public function refactor(Node $node): Node + { + return $node; + } +} diff --git a/tests/PhpParser/NodeTraverser/Function_/RuleUsingFunctionRector.php b/tests/PhpParser/NodeTraverser/Function_/RuleUsingFunctionRector.php new file mode 100644 index 00000000000..6c23a37406d --- /dev/null +++ b/tests/PhpParser/NodeTraverser/Function_/RuleUsingFunctionRector.php @@ -0,0 +1,35 @@ +> + */ + public function getNodeTypes(): array + { + return [Function_::class]; + } + + public function refactor(Node $node): Node + { + return $node; + } +} diff --git a/tests/PhpParser/NodeTraverser/RectorNodeTraverserTest.php b/tests/PhpParser/NodeTraverser/RectorNodeTraverserTest.php new file mode 100644 index 00000000000..6bdb57b7644 --- /dev/null +++ b/tests/PhpParser/NodeTraverser/RectorNodeTraverserTest.php @@ -0,0 +1,90 @@ +rectorNodeTraverser = $this->make(RectorNodeTraverser::class); + $this->rectorNodeTraverser->refreshPhpRectors([]); + + $this->ruleUsingFunctionRector = new RuleUsingFunctionRector(); + $this->ruleUsingClassRector = new RuleUsingClassRector(); + $this->ruleUsingClassLikeRector = new RuleUsingClassLikeRector(); + } + + public function testGetVisitorsForNodeWhenNoVisitorsAvailable(): void + { + $class = new Class_('test'); + + $visitors = $this->rectorNodeTraverser->getVisitorsForNode($class); + + $this->assertSame([], $visitors); + } + + public function testGetVisitorsForNodeWhenNoVisitorsMatch(): void + { + $class = new Class_('test'); + $this->rectorNodeTraverser->addVisitor($this->ruleUsingFunctionRector); + + $visitors = $this->rectorNodeTraverser->getVisitorsForNode($class); + + $this->assertSame([], $visitors); + } + + public function testGetVisitorsForNodeWhenSomeVisitorsMatch(): void + { + $class = new Class_('test'); + $this->rectorNodeTraverser->addVisitor($this->ruleUsingFunctionRector); + $this->rectorNodeTraverser->addVisitor($this->ruleUsingClassRector); + + $visitors = $this->rectorNodeTraverser->getVisitorsForNode($class); + + $this->assertEquals([$this->ruleUsingClassRector], $visitors); + } + + public function testGetVisitorsForNodeWhenAllVisitorsMatch(): void + { + $class = new Class_('test'); + $this->rectorNodeTraverser->addVisitor($this->ruleUsingClassRector); + $this->rectorNodeTraverser->addVisitor($this->ruleUsingClassLikeRector); + + $visitors = $this->rectorNodeTraverser->getVisitorsForNode($class); + + $this->assertEquals([$this->ruleUsingClassRector, $this->ruleUsingClassLikeRector], $visitors); + } + + public function testGetVisitorsForNodeUsesCachedValue(): void + { + $class = new Class_('test'); + $this->rectorNodeTraverser->addVisitor($this->ruleUsingClassRector); + $this->rectorNodeTraverser->addVisitor($this->ruleUsingClassLikeRector); + + $visitors = $this->rectorNodeTraverser->getVisitorsForNode($class); + + $this->assertEquals([$this->ruleUsingClassRector, $this->ruleUsingClassLikeRector], $visitors); + + $this->rectorNodeTraverser->removeVisitor($this->ruleUsingClassRector); + $visitors = $this->rectorNodeTraverser->getVisitorsForNode($class); + + $this->assertEquals([$this->ruleUsingClassRector, $this->ruleUsingClassLikeRector], $visitors); + } +}