From b04ab50ee12885d384086f7fb2ca865b84d72b7d Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 14 Jan 2026 17:20:23 +0100 Subject: [PATCH] [int] merge AbstractImmutableNodeTraverser and RectorNodeTraverser as same contents --- phpstan.neon | 8 +- .../AbstractImmutableNodeTraverser.php | 312 ------------------ .../NodeTraverser/RectorNodeTraverser.php | 302 ++++++++++++++++- .../NodeTraverser/RectorNodeTraverserTest.php | 2 +- 4 files changed, 304 insertions(+), 320 deletions(-) delete mode 100644 src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php diff --git a/phpstan.neon b/phpstan.neon index a6728867c6c..b683b853111 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -375,14 +375,10 @@ parameters: # copied from /vendor, to keep as original as possible - identifier: symplify.noDynamicName - path: src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php + path: src/PhpParser/NodeTraverser/RectorNodeTraverser.php - identifier: offsetAccess.nonArray - path: src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php - - - - message: '#Property Rector\\PhpParser\\NodeTraverser\\AbstractImmutableNodeTraverser\:\:\$visitors \(list\) does not accept array#' - path: src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php + path: src/PhpParser/NodeTraverser/RectorNodeTraverser.php # false positive - diff --git a/src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php b/src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php deleted file mode 100644 index e27caade8ef..00000000000 --- a/src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php +++ /dev/null @@ -1,312 +0,0 @@ - Visitors - */ - protected array $visitors = []; - - /** - * @var bool Whether traversal should be stopped - */ - protected bool $stopTraversal; - - private bool $areNodeVisitorsPrepared = false; - - /** - * @var array, NodeVisitor[]> - */ - private array $visitorsPerNodeClass = []; - - /** - * @param RectorInterface[] $rectors - */ - public function __construct( - private readonly PhpVersionedFilter $phpVersionedFilter, - private readonly ConfigurationRuleFilter $configurationRuleFilter, - private array $rectors - ) { - } - - public function addVisitor(NodeVisitor $visitor): void - { - throw new ShouldNotHappenException('The immutable node traverser does not support adding visitors.'); - } - - public function removeVisitor(NodeVisitor $visitor): void - { - throw new ShouldNotHappenException('The immutable node traverser does not support removing visitors.'); - } - - /** - * @param Node[] $nodes - * @return Node[] - */ - public function traverse(array $nodes): array - { - $this->prepareNodeVisitors(); - - $this->stopTraversal = false; - foreach ($this->visitors as $visitor) { - if (null !== $return = $visitor->beforeTraverse($nodes)) { - $nodes = $return; - } - } - - $nodes = $this->traverseArray($nodes); - for ($i = \count($this->visitors) - 1; $i >= 0; --$i) { - $visitor = $this->visitors[$i]; - if (null !== $return = $visitor->afterTraverse($nodes)) { - $nodes = $return; - } - } - - return $nodes; - } - - /** - * @param RectorInterface[] $rectors - * @api used in tests to update the active rules - * - * @internal Used only in Rector core, not supported outside. Might change any time. - */ - public function refreshPhpRectors(array $rectors): void - { - Assert::allIsInstanceOf($rectors, RectorInterface::class); - - $this->rectors = $rectors; - $this->visitors = []; - $this->visitorsPerNodeClass = []; - - $this->areNodeVisitorsPrepared = false; - - $this->prepareNodeVisitors(); - } - - /** - * @return NodeVisitor[] - */ - public function getVisitorsForNode(Node $node): array - { - $nodeClass = $node::class; - - if (! isset($this->visitorsPerNodeClass[$nodeClass])) { - $this->visitorsPerNodeClass[$nodeClass] = []; - - /** @var RectorInterface $visitor */ - foreach ($this->visitors as $visitor) { - foreach ($visitor->getNodeTypes() as $nodeType) { - // BC layer matching - if ($nodeType === FileWithoutNamespace::class && $nodeClass === FileNode::class) { - $this->visitorsPerNodeClass[$nodeClass][] = $visitor; - continue; - } - - if (is_a($nodeClass, $nodeType, true)) { - $this->visitorsPerNodeClass[$nodeClass][] = $visitor; - continue 2; - } - } - } - } - - return $this->visitorsPerNodeClass[$nodeClass]; - } - - protected function traverseNode(Node $node): void - { - foreach ($node->getSubNodeNames() as $name) { - $subNode = $node->{$name}; - if (\is_array($subNode)) { - $node->{$name} = $this->traverseArray($subNode); - if ($this->stopTraversal) { - break; - } - - continue; - } - - if (! $subNode instanceof Node) { - continue; - } - - $traverseChildren = true; - $currentNodeVisitors = $this->getVisitorsForNode($subNode); - - foreach ($currentNodeVisitors as $currentNodeVisitor) { - $return = $currentNodeVisitor->enterNode($subNode); - if ($return !== null) { - if ($return instanceof Node) { - $originalSubNodeClass = $subNode::class; - - $this->ensureReplacementReasonable($subNode, $return); - $subNode = $return; - $node->{$name} = $return; - - if ($originalSubNodeClass !== $subNode::class) { - // stop traversing as node type changed and visitors won't work - continue 2; - } - - } elseif ($return === NodeVisitor::DONT_TRAVERSE_CHILDREN) { - $traverseChildren = false; - } elseif ($return === NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN) { - $traverseChildren = false; - break; - } elseif ($return === NodeVisitor::STOP_TRAVERSAL) { - $this->stopTraversal = true; - break 2; - } elseif ($return === NodeVisitor::REPLACE_WITH_NULL) { - $node->{$name} = null; - continue 2; - } else { - throw new LogicException('enterNode() returned invalid value of type ' . gettype($return)); - } - } - } - - if ($traverseChildren) { - $this->traverseNode($subNode); - if ($this->stopTraversal) { - break; - } - } - } - } - - /** - * @param Node[] $nodes - * @return Node[] - */ - protected function traverseArray(array $nodes): array - { - $doNodes = []; - foreach ($nodes as $i => $node) { - if (! $node instanceof Node) { - if (\is_array($node)) { - throw new LogicException('Invalid node structure: Contains nested arrays'); - } - - continue; - } - - $traverseChildren = true; - $currentNodeVisitors = $this->getVisitorsForNode($node); - - foreach ($currentNodeVisitors as $currentNodeVisitor) { - $return = $currentNodeVisitor->enterNode($node); - if ($return !== null) { - if ($return instanceof Node) { - $originalNodeNodeClass = $node::class; - $this->ensureReplacementReasonable($node, $return); - $nodes[$i] = $node = $return; - - if ($originalNodeNodeClass !== $return::class) { - // stop traversing as node type changed and visitors won't work - continue 2; - } - } elseif (\is_array($return)) { - $doNodes[] = [$i, $return]; - continue 2; - } elseif ($return === NodeVisitor::REMOVE_NODE) { - $doNodes[] = [$i, []]; - continue 2; - } elseif ($return === NodeVisitor::DONT_TRAVERSE_CHILDREN) { - $traverseChildren = false; - } elseif ($return === NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN) { - $traverseChildren = false; - break; - } elseif ($return === NodeVisitor::STOP_TRAVERSAL) { - $this->stopTraversal = true; - break 2; - } elseif ($return === NodeVisitor::REPLACE_WITH_NULL) { - throw new LogicException( - 'REPLACE_WITH_NULL can not be used if the parent structure is an array' - ); - } else { - throw new LogicException('enterNode() returned invalid value of type ' . gettype($return)); - } - } - } - - if ($traverseChildren) { - $this->traverseNode($node); - if ($this->stopTraversal) { - break; - } - } - } - - if ($doNodes !== []) { - while ([$i, $replace] = array_pop($doNodes)) { - array_splice($nodes, $i, 1, $replace); - } - } - - return $nodes; - } - - private function ensureReplacementReasonable(Node $old, Node $new): void - { - if ($old instanceof Stmt) { - if ($new instanceof Expr) { - throw new LogicException( - sprintf('Trying to replace statement (%s) ', $old->getType()) . sprintf( - 'with expression (%s). Are you missing a ', - $new->getType() - ) . 'Stmt_Expression wrapper?' - ); - } - - return; - } - - if ($new instanceof Stmt) { - throw new LogicException( - sprintf('Trying to replace expression (%s) ', $old->getType()) . sprintf( - 'with statement (%s)', - $new->getType() - ) - ); - } - } - - /** - * This must happen after $this->configuration is set after ProcessCommand::execute() is run, otherwise we get default false positives. - * - * This should be removed after https://github.com/rectorphp/rector/issues/5584 is resolved - */ - private function prepareNodeVisitors(): void - { - if ($this->areNodeVisitorsPrepared) { - return; - } - - // filer out by version - $this->visitors = $this->phpVersionedFilter->filter($this->rectors); - - // filter by configuration - $this->visitors = $this->configurationRuleFilter->filter($this->visitors); - - $this->areNodeVisitorsPrepared = true; - } -} diff --git a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php index 14c323ee00e..f8bb6c4dba2 100644 --- a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php +++ b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php @@ -4,9 +4,309 @@ namespace Rector\PhpParser\NodeTraverser; +use LogicException; +use PhpParser\Node; +use PhpParser\Node\Expr; +use PhpParser\Node\Stmt; +use PhpParser\NodeTraverserInterface; +use PhpParser\NodeVisitor; +use Rector\Configuration\ConfigurationRuleFilter; +use Rector\Contract\Rector\RectorInterface; +use Rector\Exception\ShouldNotHappenException; +use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; +use Rector\VersionBonding\PhpVersionedFilter; +use Webmozart\Assert\Assert; + /** * @see \Rector\Tests\PhpParser\NodeTraverser\RectorNodeTraverserTest */ -final class RectorNodeTraverser extends AbstractImmutableNodeTraverser +final class RectorNodeTraverser implements NodeTraverserInterface { + /** + * @var list + */ + private array $visitors = []; + + private bool $stopTraversal; + + private bool $areNodeVisitorsPrepared = false; + + /** + * @var array, NodeVisitor[]> + */ + private array $visitorsPerNodeClass = []; + + /** + * @param RectorInterface[] $rectors + */ + public function __construct( + private readonly PhpVersionedFilter $phpVersionedFilter, + private readonly ConfigurationRuleFilter $configurationRuleFilter, + private array $rectors + ) { + } + + public function addVisitor(NodeVisitor $visitor): void + { + throw new ShouldNotHappenException('The immutable node traverser does not support adding visitors.'); + } + + public function removeVisitor(NodeVisitor $visitor): void + { + throw new ShouldNotHappenException('The immutable node traverser does not support removing visitors.'); + } + + /** + * @param Node[] $nodes + * @return Node[] + */ + public function traverse(array $nodes): array + { + $this->prepareNodeVisitors(); + + $this->stopTraversal = false; + foreach ($this->visitors as $visitor) { + if (null !== $return = $visitor->beforeTraverse($nodes)) { + $nodes = $return; + } + } + + $nodes = $this->traverseArray($nodes); + for ($i = \count($this->visitors) - 1; $i >= 0; --$i) { + $visitor = $this->visitors[$i]; + if (null !== $return = $visitor->afterTraverse($nodes)) { + $nodes = $return; + } + } + + return $nodes; + } + + /** + * @param RectorInterface[] $rectors + * @api used in tests to update the active rules + * + * @internal Used only in Rector core, not supported outside. Might change any time. + */ + public function refreshPhpRectors(array $rectors): void + { + Assert::allIsInstanceOf($rectors, RectorInterface::class); + + $this->rectors = $rectors; + $this->visitors = []; + $this->visitorsPerNodeClass = []; + + $this->areNodeVisitorsPrepared = false; + + $this->prepareNodeVisitors(); + } + + /** + * @return NodeVisitor[] + */ + public function getVisitorsForNode(Node $node): array + { + $nodeClass = $node::class; + + if (! isset($this->visitorsPerNodeClass[$nodeClass])) { + $this->visitorsPerNodeClass[$nodeClass] = []; + + /** @var RectorInterface $visitor */ + foreach ($this->visitors as $visitor) { + foreach ($visitor->getNodeTypes() as $nodeType) { + // BC layer matching + if ($nodeType === FileWithoutNamespace::class && $nodeClass === FileNode::class) { + $this->visitorsPerNodeClass[$nodeClass][] = $visitor; + continue; + } + + if (is_a($nodeClass, $nodeType, true)) { + $this->visitorsPerNodeClass[$nodeClass][] = $visitor; + continue 2; + } + } + } + } + + return $this->visitorsPerNodeClass[$nodeClass]; + } + + private function traverseNode(Node $node): void + { + foreach ($node->getSubNodeNames() as $name) { + $subNode = $node->{$name}; + if (\is_array($subNode)) { + $node->{$name} = $this->traverseArray($subNode); + if ($this->stopTraversal) { + break; + } + + continue; + } + + if (! $subNode instanceof Node) { + continue; + } + + $traverseChildren = true; + $currentNodeVisitors = $this->getVisitorsForNode($subNode); + + foreach ($currentNodeVisitors as $currentNodeVisitor) { + $return = $currentNodeVisitor->enterNode($subNode); + if ($return !== null) { + if ($return instanceof Node) { + $originalSubNodeClass = $subNode::class; + + $this->ensureReplacementReasonable($subNode, $return); + $subNode = $return; + $node->{$name} = $return; + + if ($originalSubNodeClass !== $subNode::class) { + // stop traversing as node type changed and visitors won't work + continue 2; + } + + } elseif ($return === NodeVisitor::DONT_TRAVERSE_CHILDREN) { + $traverseChildren = false; + } elseif ($return === NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN) { + $traverseChildren = false; + break; + } elseif ($return === NodeVisitor::STOP_TRAVERSAL) { + $this->stopTraversal = true; + break 2; + } elseif ($return === NodeVisitor::REPLACE_WITH_NULL) { + $node->{$name} = null; + continue 2; + } else { + throw new LogicException('enterNode() returned invalid value of type ' . gettype($return)); + } + } + } + + if ($traverseChildren) { + $this->traverseNode($subNode); + if ($this->stopTraversal) { + break; + } + } + } + } + + /** + * @param Node[] $nodes + * @return Node[] + */ + private function traverseArray(array $nodes): array + { + $doNodes = []; + foreach ($nodes as $i => $node) { + if (! $node instanceof Node) { + if (\is_array($node)) { + throw new LogicException('Invalid node structure: Contains nested arrays'); + } + + continue; + } + + $traverseChildren = true; + $currentNodeVisitors = $this->getVisitorsForNode($node); + + foreach ($currentNodeVisitors as $currentNodeVisitor) { + $return = $currentNodeVisitor->enterNode($node); + if ($return !== null) { + if ($return instanceof Node) { + $originalNodeNodeClass = $node::class; + $this->ensureReplacementReasonable($node, $return); + $nodes[$i] = $node = $return; + + if ($originalNodeNodeClass !== $return::class) { + // stop traversing as node type changed and visitors won't work + continue 2; + } + } elseif (\is_array($return)) { + $doNodes[] = [$i, $return]; + continue 2; + } elseif ($return === NodeVisitor::REMOVE_NODE) { + $doNodes[] = [$i, []]; + continue 2; + } elseif ($return === NodeVisitor::DONT_TRAVERSE_CHILDREN) { + $traverseChildren = false; + } elseif ($return === NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN) { + $traverseChildren = false; + break; + } elseif ($return === NodeVisitor::STOP_TRAVERSAL) { + $this->stopTraversal = true; + break 2; + } elseif ($return === NodeVisitor::REPLACE_WITH_NULL) { + throw new LogicException( + 'REPLACE_WITH_NULL can not be used if the parent structure is an array' + ); + } else { + throw new LogicException('enterNode() returned invalid value of type ' . gettype($return)); + } + } + } + + if ($traverseChildren) { + $this->traverseNode($node); + if ($this->stopTraversal) { + break; + } + } + } + + if ($doNodes !== []) { + while ([$i, $replace] = array_pop($doNodes)) { + array_splice($nodes, $i, 1, $replace); + } + } + + return $nodes; + } + + private function ensureReplacementReasonable(Node $old, Node $new): void + { + if ($old instanceof Stmt) { + if ($new instanceof Expr) { + throw new LogicException( + sprintf('Trying to replace statement (%s) ', $old->getType()) . sprintf( + 'with expression (%s). Are you missing a ', + $new->getType() + ) . 'Stmt_Expression wrapper?' + ); + } + + return; + } + + if ($new instanceof Stmt) { + throw new LogicException( + sprintf('Trying to replace expression (%s) ', $old->getType()) . sprintf( + 'with statement (%s)', + $new->getType() + ) + ); + } + } + + /** + * This must happen after $this->configuration is set after ProcessCommand::execute() is run, otherwise we get default false positives. + * + * This should be removed after https://github.com/rectorphp/rector/issues/5584 is resolved + */ + private function prepareNodeVisitors(): void + { + if ($this->areNodeVisitorsPrepared) { + return; + } + + // filer out by version + $this->visitors = $this->phpVersionedFilter->filter($this->rectors); + + // filter by configuration + $this->visitors = $this->configurationRuleFilter->filter($this->visitors); + + $this->areNodeVisitorsPrepared = true; + } } diff --git a/tests/PhpParser/NodeTraverser/RectorNodeTraverserTest.php b/tests/PhpParser/NodeTraverser/RectorNodeTraverserTest.php index 35b4a074abd..d7e9208ae7d 100644 --- a/tests/PhpParser/NodeTraverser/RectorNodeTraverserTest.php +++ b/tests/PhpParser/NodeTraverser/RectorNodeTraverserTest.php @@ -12,7 +12,7 @@ use Rector\Tests\PhpParser\NodeTraverser\Function_\RuleUsingFunctionRector; /** - * @see \Rector\PhpParser\NodeTraverser\AbstractImmutableNodeTraverser + * @see \Rector\PhpParser\NodeTraverser\RectorNodeTraverser */ final class RectorNodeTraverserTest extends AbstractLazyTestCase {