From d48ab7cd1112f3424d30ebeb41a1e325ba00ce86 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 18 Nov 2025 16:01:34 +0000 Subject: [PATCH 1/3] extract NodeTraverser from vendor, make it immutable and make it part of rector-src to avoid patching --- .../copy-and-create-immuatable-node-visitor.php | 15 +++++++++++++++ .../NodeTraverser/RectorNodeTraverser.php | 10 ++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 scripts/copy-and-create-immuatable-node-visitor.php diff --git a/scripts/copy-and-create-immuatable-node-visitor.php b/scripts/copy-and-create-immuatable-node-visitor.php new file mode 100644 index 00000000000..e14b3a72d9d --- /dev/null +++ b/scripts/copy-and-create-immuatable-node-visitor.php @@ -0,0 +1,15 @@ +visitorsPerNodeClass[$nodeClass])) { $this->visitorsPerNodeClass[$nodeClass] = []; foreach ($this->visitors as $visitor) { - assert($visitor instanceof RectorInterface); + // already checked in prepare visitors method + /** @var RectorInterface $visitor */ foreach ($visitor->getNodeTypes() as $nodeType) { if (is_a($nodeClass, $nodeType, true)) { $this->visitorsPerNodeClass[$nodeClass][] = $visitor; @@ -85,10 +88,9 @@ public function getVisitorsForNode(Node $node): array } /** - * This must happen after $this->configuration is set after ProcessCommand::execute() is run, - * otherwise we get default false positives. + * This must happen after $this->configuration is set after ProcessCommand::execute() is run, otherwise we get default false positives. * - * This hack should be removed after https://github.com/rectorphp/rector/issues/5584 is resolved + * This should be removed after https://github.com/rectorphp/rector/issues/5584 is resolved */ private function prepareNodeVisitors(): void { From a53aa7e73291b59d1100e3f66e7f45f24c347adb Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 18 Nov 2025 16:02:46 +0000 Subject: [PATCH 2/3] [dx] immutable node visitor --- composer.json | 3 +- phpstan.neon | 24 +- rector.php | 3 +- .../avoid-short-node-names-in-fixtures.php | 14 +- scripts/check-before-after-same-fixtures.php | 3 +- ...opy-and-create-immuatable-node-visitor.php | 15 - scripts/create-immutable-node-visitor.php | 263 +++++++++++++++ scripts/src/Finder/RectorClassFinder.php | 3 +- .../AbstractImmutableNodeTraverser.php | 306 ++++++++++++++++++ .../NodeTraverser/RectorNodeTraverser.php | 16 +- .../NodeTraverser/RectorNodeTraverserTest.php | 3 + 11 files changed, 616 insertions(+), 37 deletions(-) delete mode 100644 scripts/copy-and-create-immuatable-node-visitor.php create mode 100644 scripts/create-immutable-node-visitor.php create mode 100644 src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php diff --git a/composer.json b/composer.json index bef14824cdd..c4708829e3e 100644 --- a/composer.json +++ b/composer.json @@ -133,8 +133,7 @@ "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-nodetraverser-php.patch" + "https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/nikic-php-parser-lib-phpparser-node-stmt-namespace-php.patch" ] }, "composer-exit-on-patch-failure": true, diff --git a/phpstan.neon b/phpstan.neon index cc7db529073..38f660b0e2e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -217,7 +217,6 @@ parameters: # optional as changes behavior, should be used explicitly outside PHP upgrade - '#Register "Rector\\Php73\\Rector\\FuncCall\\JsonThrowOnErrorRector" service to "php73\.php" config set#' - - '#Register "Rector\\Php81\\Rector\\ClassMethod\\NewInInitializerRector" service to "php81\.php" config set#' - '#Register "Rector\\Php80\\Rector\\NotIdentical\\MbStrContainsRector" service to "php80\.php" config set#' - '#Register "Rector\\Php85\\Rector\\StmtsAwareInterface\\SequentialAssignmentsToPipeOperatorRector" service to "php85\.php" config set#' - '#Register "Rector\\Php85\\Rector\\Expression\\NestedFuncCallsToPipeOperatorRector" service to "php85\.php" config set#' @@ -356,6 +355,27 @@ parameters: - '#Method Rector\\Utils\\Rector\\RemoveRefactorDuplicatedNodeInstanceCheckRector\:\:getInstanceofNodeClass\(\) should return class\-string\|null but returns class\-string#' + # helper classes for bin script - - path: bin/list-unused-rules.php + path: scripts identifier: symplify.multipleClassLikeInFile + + # copied from /vendor, to keep as original as possible + - + identifier: symplify.forbiddenNode + path: src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php + - + identifier: missingType.iterableValue + path: src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php + - + identifier: symplify.noDynamicName + path: src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php + - + identifier: offsetAccess.nonArray + path: src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php + - + message: '#Property PhpParser\\Node\\Stmt\\ClassMethod\:\:\$stmts \(array\|null\) does not accept array#' + path: scripts/create-immutable-node-visitor.php + - + message: '#Property Rector\\PhpParser\\NodeTraverser\\AbstractImmutableNodeTraverser\:\:\$visitors \(list\) does not accept array#' + path: src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php diff --git a/rector.php b/rector.php index 308bcd21a5d..20e9a368c50 100644 --- a/rector.php +++ b/rector.php @@ -28,12 +28,13 @@ ->withPhpSets() ->withPaths([ __DIR__ . '/bin', + __DIR__ . '/config', __DIR__ . '/src', __DIR__ . '/rules', __DIR__ . '/rules-tests', __DIR__ . '/tests', __DIR__ . '/utils', - __DIR__ . '/config', + __DIR__ . '/scripts', __DIR__ . '/build/build-preload.php', ]) ->withRootFiles() diff --git a/scripts/avoid-short-node-names-in-fixtures.php b/scripts/avoid-short-node-names-in-fixtures.php index 78dbc511b63..3c488da66b4 100644 --- a/scripts/avoid-short-node-names-in-fixtures.php +++ b/scripts/avoid-short-node-names-in-fixtures.php @@ -9,6 +9,8 @@ use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; require __DIR__ . '/../vendor/autoload.php'; @@ -16,12 +18,12 @@ // get short node names -$nodeFileFinder = \Symfony\Component\Finder\Finder::create() +$nodeFileFinder = Finder::create() ->files() ->name('*.php') ->in(__DIR__ . '/../vendor/nikic/php-parser/lib/PhpParser/Node'); -/** @var \Symfony\Component\Finder\SplFileInfo[] $nodeFileInfos */ +/** @var SplFileInfo[] $nodeFileInfos */ $nodeFileInfos = iterator_to_array($nodeFileFinder->getIterator()); $shortNodeClassNames = []; @@ -33,10 +35,10 @@ $hasErrors = false; -foreach ($fixtureFiles as $fixtureFileInfo) { +foreach ($fixtureFiles as $fixtureFile) { $shortClassNameMatch = Strings::match( - $fixtureFileInfo->getContents(), - '/\b(?:class|interface)\s+(?[A-Z][A-Za-z0-9_]*)/' + $fixtureFile->getContents(), + '/\b(?:class|interface)\s+(?[A-Z]\w*)/' ); if ($shortClassNameMatch === null) { continue; @@ -52,7 +54,7 @@ $fixtureClassName, PHP_EOL, PHP_EOL, - $fixtureFileInfo->getRealPath(), + $fixtureFile->getRealPath(), PHP_EOL )); diff --git a/scripts/check-before-after-same-fixtures.php b/scripts/check-before-after-same-fixtures.php index 63ed8c02def..27f207e2781 100644 --- a/scripts/check-before-after-same-fixtures.php +++ b/scripts/check-before-after-same-fixtures.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Nette\Utils\Strings; +use Rector\Scripts\Finder\FixtureFinder; require __DIR__ . '/../vendor/autoload.php'; @@ -27,7 +28,7 @@ public function __construct() */ public function run(array $testDirectories): int { - $fixtureFiles = \Rector\Scripts\Finder\FixtureFinder::find($testDirectories); + $fixtureFiles = FixtureFinder::find($testDirectories); $invalidFixturePaths = []; foreach ($fixtureFiles as $fixtureFile) { diff --git a/scripts/copy-and-create-immuatable-node-visitor.php b/scripts/copy-and-create-immuatable-node-visitor.php deleted file mode 100644 index e14b3a72d9d..00000000000 --- a/scripts/copy-and-create-immuatable-node-visitor.php +++ /dev/null @@ -1,15 +0,0 @@ -createForHostVersion(); +$stmts = $parser->parse(FileSystem::read($vendorNodeTraverserFilePath)); + +Assert::isArray($stmts); +Assert::allIsInstanceOf($stmts, Stmt::class); + +$originalStmts = $stmts; + +final class ReplaceForeachThisVisitorNodeVisitor extends NodeVisitorAbstract +{ + public function __construct( + private readonly string $nodeName + ) { + + } + + /** + * @return Stmt[]|null + */ + public function enterNode(Node $node): ?array + { + if (! $node instanceof Foreach_) { + return null; + } + + if (! $node->expr instanceof PropertyFetch) { + return null; + } + + $foreachedPropertyFetch = $node->expr; + if (! $foreachedPropertyFetch->var instanceof Variable) { + return null; + } + + if ($foreachedPropertyFetch->var->name !== 'this') { + return null; + } + + if (! $foreachedPropertyFetch->name instanceof Identifier) { + return null; + } + + if ($foreachedPropertyFetch->name->toString() !== 'visitors') { + return null; + } + + // replace $this->visitors with $currentNodeVisitors + $currentNodeVisitorsVariable = new Variable(ImmutableNodeTraverserName::CURRENT_NODE_VISITORS); + $node->expr = $currentNodeVisitorsVariable; + + // add before foreach: $currentNodeVisitors = $this->getVisitorsForNode($node); + $assign = new Assign( + $currentNodeVisitorsVariable, + new MethodCall( + new Variable('this'), + ImmutableNodeTraverserName::GET_VISITORS_FOR_NODE_METHOD, + [new Arg(new Variable($this->nodeName))] + ) + ); + + return [new Expression($assign), $node]; + } +} + +final class ReplaceThisVisitorsWithThisGetVisitorsNodeVisitor extends NodeVisitorAbstract +{ + public function enterNode(Node $node): ?ClassMethod + { + if (! $node instanceof ClassMethod || $node->stmts === null) { + return null; + } + + if (! in_array($node->name->toString(), ['traverseArray', 'traverseNode'])) { + return null; + } + + $traverseArrayNodeName = $node->name->toString() === 'traverseNode' ? 'subNode' : 'node'; + + // handle foreach $this->visitors + $nodeTraverser = new NodeTraverser(); + $nodeTraverser->addVisitor(new ReplaceForeachThisVisitorNodeVisitor($traverseArrayNodeName)); + + $node->stmts = $nodeTraverser->traverse($node->stmts); + + return $node; + } +} + +final class RenameNamespaceNodeVisitor extends NodeVisitorAbstract +{ + public function enterNode(Node $node): ?Namespace_ + { + if (! $node instanceof Namespace_) { + return null; + } + + // add uses for PHPParser nodes as locations are now changed + $uses = [ + new Use_([new UseItem(new Name(NodeTraverserInterface::class))]), + new Use_([new UseItem(new Name(NodeVisitor::class))]), + new Use_([new UseItem(new Name(Node::class))]), + ]; + + /** @var Stmt[] $newStmts */ + $newStmts = array_merge($uses, (array) $node->stmts); + $node->stmts = $newStmts; + + $node->name = new Name('Rector\PhpParser\NodeTraverser'); + return $node; + } +} + +final class ReplaceThisVisitorsArrayDimFetchWithCurrentNodeVisitorsNodeVisitor extends NodeVisitorAbstract +{ + public function enterNode(Node $node): ?Node + { + if (! $node instanceof ArrayDimFetch) { + return null; + } + + if (! $node->var instanceof PropertyFetch) { + return null; + } + + $propertyFetch = $node->var; + if (! $propertyFetch->var instanceof Variable) { + return null; + } + + if ($propertyFetch->var->name !== 'this') { + return null; + } + + if (! $propertyFetch->name instanceof Identifier) { + return null; + } + + if ($propertyFetch->name->toString() !== 'visitors') { + return null; + } + + if (! $node->dim instanceof Variable) { + return null; + } + + if ($node->dim->name !== 'visitorIndex') { + return null; + } + + $node->var = new Variable(ImmutableNodeTraverserName::CURRENT_NODE_VISITORS); + + return $node; + } +} + +final class DecorateClassNodeVisitor extends NodeVisitorAbstract +{ + public function enterNode(Node $node): ?Class_ + { + if (! $node instanceof Class_) { + return null; + } + + $node->flags |= Modifiers::ABSTRACT; + $node->name = new Identifier('AbstractImmutableNodeTraverser'); + + $getVisitorsForNodeClassMethod = new ClassMethod(ImmutableNodeTraverserName::GET_VISITORS_FOR_NODE_METHOD, [ + 'flags' => Modifiers::PUBLIC | Modifiers::ABSTRACT, + 'params' => [new Param(var: new Variable('node'), type: new FullyQualified(Node::class))], + 'returnType' => new Name('array'), + 'stmts' => null, + ]); + + // add @return NodeVisitor[] docblock + $getVisitorsForNodeClassMethod->setDocComment(new Doc(<<<'DOC' +/** + * @return NodeVisitor[] + */ +DOC + )); + + $node->stmts[] = $getVisitorsForNodeClassMethod; + + return $node; + } +} + +$nodeTraverser = new NodeTraverser(); + +$nodeTraverser->addVisitor(new DecorateClassNodeVisitor()); +$nodeTraverser->addVisitor(new ReplaceThisVisitorsWithThisGetVisitorsNodeVisitor()); +$nodeTraverser->addVisitor(new ReplaceThisVisitorsArrayDimFetchWithCurrentNodeVisitorsNodeVisitor()); +$nodeTraverser->addVisitor(new RenameNamespaceNodeVisitor()); + +$stmts = $nodeTraverser->traverse($stmts); + +final class ImmutableNodeTraverserName +{ + /** + * @var string + */ + public const GET_VISITORS_FOR_NODE_METHOD = 'getVisitorsForNode'; + + /** + * @var string + */ + public const CURRENT_NODE_VISITORS = 'currentNodeVisitors'; +} + +// print node traverser contents +$standard = new Standard(); +$immutableNodeTraverserFileContents = $standard->printFormatPreserving($stmts, $originalStmts, $parser->getTokens()); + +// save the file +FileSystem::write( + __DIR__ . '/../src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php', + $immutableNodeTraverserFileContents +); + +echo sprintf('New file "%s" was created', 'src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php') . PHP_EOL; diff --git a/scripts/src/Finder/RectorClassFinder.php b/scripts/src/Finder/RectorClassFinder.php index e265784fecd..dbc1b39c57a 100644 --- a/scripts/src/Finder/RectorClassFinder.php +++ b/scripts/src/Finder/RectorClassFinder.php @@ -6,6 +6,7 @@ use Nette\Loaders\RobotLoader; use Rector\Configuration\Deprecation\Contract\DeprecatedInterface; +use ReflectionClass; final class RectorClassFinder { @@ -29,7 +30,7 @@ public function find(array $dirs): array // remove deprecated and abstract classes foreach ($rectorClasses as $rectorClass) { - $rectorClassReflection = new \ReflectionClass($rectorClass); + $rectorClassReflection = new ReflectionClass($rectorClass); if ($rectorClassReflection->isAbstract()) { continue; } diff --git a/src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php b/src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php new file mode 100644 index 00000000000..0aeea1d7918 --- /dev/null +++ b/src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php @@ -0,0 +1,306 @@ + Visitors + */ + protected array $visitors = []; + + /** + * @var bool Whether traversal should be stopped + */ + protected bool $stopTraversal; + + /** + * Create a traverser with the given visitors. + * + * @param NodeVisitor ...$visitors Node visitors + */ + public function __construct(NodeVisitor ...$visitors) + { + $this->visitors = $visitors; + } + + /** + * Adds a visitor. + * + * @param NodeVisitor $visitor Visitor to add + */ + public function addVisitor(NodeVisitor $visitor): void + { + $this->visitors[] = $visitor; + } + + /** + * Removes an added visitor. + */ + public function removeVisitor(NodeVisitor $visitor): void + { + $index = array_search($visitor, $this->visitors, true); + if ($index !== false) { + array_splice($this->visitors, $index, 1, []); + } + } + + /** + * Traverses an array of nodes using the registered visitors. + * + * @param Node[] $nodes Array of nodes + * + * @return Node[] Traversed array of nodes + */ + public function traverse(array $nodes): array + { + $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; + } + + /** + * @return NodeVisitor[] + */ + abstract public function getVisitorsForNode(Node $node): array; + + /** + * Recursively traverse a node. + * + * @param Node $node Node to traverse. + */ + 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; + $visitorIndex = -1; + $currentNodeVisitors = $this->getVisitorsForNode($subNode); + foreach ($currentNodeVisitors as $visitorIndex => $visitor) { + $return = $visitor->enterNode($subNode); + if ($return !== null) { + if ($return instanceof Node) { + $this->ensureReplacementReasonable($subNode, $return); + $subNode = $return; + $node->{$name} = $return; + } 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; + } + } + + for (; $visitorIndex >= 0; --$visitorIndex) { + $visitor = $currentNodeVisitors[$visitorIndex]; + $return = $visitor->leaveNode($subNode); + if ($return !== null) { + if ($return instanceof Node) { + $this->ensureReplacementReasonable($subNode, $return); + $subNode = $return; + $node->{$name} = $return; + } elseif ($return === NodeVisitor::STOP_TRAVERSAL) { + $this->stopTraversal = true; + break 2; + } elseif ($return === NodeVisitor::REPLACE_WITH_NULL) { + $node->{$name} = null; + break; + } elseif (\is_array($return)) { + throw new LogicException( + 'leaveNode() may only return an array if the parent structure is an array' + ); + } else { + throw new LogicException('leaveNode() returned invalid value of type ' . gettype($return)); + } + } + } + } + } + + /** + * Recursively traverse array (usually of nodes). + * + * @param Node[] $nodes Array to traverse + * + * @return array Result of traversal (may be original array or changed one) + */ + 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; + $visitorIndex = -1; + $currentNodeVisitors = $this->getVisitorsForNode($node); + foreach ($currentNodeVisitors as $visitorIndex => $visitor) { + $return = $visitor->enterNode($node); + if ($return !== null) { + if ($return instanceof Node) { + $this->ensureReplacementReasonable($node, $return); + $nodes[$i] = $node = $return; + } 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; + } + } + + for (; $visitorIndex >= 0; --$visitorIndex) { + $visitor = $currentNodeVisitors[$visitorIndex]; + $return = $visitor->leaveNode($node); + if ($return !== null) { + if ($return instanceof Node) { + $this->ensureReplacementReasonable($node, $return); + $nodes[$i] = $node = $return; + } elseif (\is_array($return)) { + $doNodes[] = [$i, $return]; + break; + } elseif ($return === NodeVisitor::REMOVE_NODE) { + $doNodes[] = [$i, []]; + 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('leaveNode() returned invalid value of type ' . gettype($return)); + } + } + } + } + + 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 && $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?' + ); + } + + if ($old instanceof Expr && $new instanceof Stmt) { + throw new LogicException( + sprintf('Trying to replace expression (%s) ', $old->getType()) . sprintf( + 'with statement (%s)', + $new->getType() + ) + ); + } + } +} diff --git a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php index f4b1bbe9052..49c1c3cdfc0 100644 --- a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php +++ b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php @@ -6,22 +6,20 @@ use PhpParser\Node; use PhpParser\Node\Stmt; -use PhpParser\NodeTraverser; use PhpParser\NodeVisitor; use Rector\Configuration\ConfigurationRuleFilter; use Rector\Contract\Rector\RectorInterface; use Rector\VersionBonding\PhpVersionedFilter; -use Webmozart\Assert\Assert; /** * @see \Rector\Tests\PhpParser\NodeTraverser\RectorNodeTraverserTest */ -final class RectorNodeTraverser extends NodeTraverser +final class RectorNodeTraverser extends AbstractImmutableNodeTraverser { private bool $areNodeVisitorsPrepared = false; /** - * @var array,RectorInterface[]> + * @var array, NodeVisitor[]> */ private array $visitorsPerNodeClass = []; @@ -61,21 +59,21 @@ public function refreshPhpRectors(array $rectors): void } /** - * 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; + static $counter = 0; + if (! isset($this->visitorsPerNodeClass[$nodeClass])) { $this->visitorsPerNodeClass[$nodeClass] = []; + /** @var RectorInterface $visitor */ foreach ($this->visitors as $visitor) { - // already checked in prepare visitors method - /** @var RectorInterface $visitor */ foreach ($visitor->getNodeTypes() as $nodeType) { + ++$counter; + if (is_a($nodeClass, $nodeType, true)) { $this->visitorsPerNodeClass[$nodeClass][] = $visitor; continue 2; diff --git a/tests/PhpParser/NodeTraverser/RectorNodeTraverserTest.php b/tests/PhpParser/NodeTraverser/RectorNodeTraverserTest.php index 9bd01fac6df..25b2462dceb 100644 --- a/tests/PhpParser/NodeTraverser/RectorNodeTraverserTest.php +++ b/tests/PhpParser/NodeTraverser/RectorNodeTraverserTest.php @@ -11,6 +11,9 @@ use Rector\Tests\PhpParser\NodeTraverser\ClassLike\RuleUsingClassLikeRector; use Rector\Tests\PhpParser\NodeTraverser\Function_\RuleUsingFunctionRector; +/** + * @see \Rector\PhpParser\NodeTraverser\AbstractImmutableNodeTraverser + */ final class RectorNodeTraverserTest extends AbstractLazyTestCase { private RectorNodeTraverser $rectorNodeTraverser; From c3f487327b77ae8d0b4119d90e9ae4152bcda0f5 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 19 Nov 2025 09:28:32 +0000 Subject: [PATCH 3/3] misc --- composer.json | 6 +++--- src/PhpParser/NodeTraverser/RectorNodeTraverser.php | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index c4708829e3e..3cf874d989f 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "ondram/ci-detector": "^4.2", "phpstan/phpdoc-parser": "^2.3", "phpstan/phpstan": "^2.1.32", - "react/event-loop": "^1.5", + "react/event-loop": "^1.6", "react/promise": "^3.3", "react/socket": "^1.16", "rector/extension-installer": "^0.11.2", @@ -39,7 +39,7 @@ "symfony/process": "^6.4", "symplify/easy-parallel": "^11.2.2", "symplify/rule-doc-generator-contracts": "^11.2", - "webmozart/assert": "^1.11" + "webmozart/assert": "^1.12" }, "require-dev": { "nette/robot-loader": "^4.1", @@ -59,7 +59,7 @@ "symplify/phpstan-rules": "^14.8", "symplify/vendor-patches": "^11.5", "tomasvotruba/class-leak": "^2.0", - "tracy/tracy": "^2.10" + "tracy/tracy": "^2.11" }, "replace": { "rector/rector": "self.version" diff --git a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php index 49c1c3cdfc0..f80922a89bc 100644 --- a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php +++ b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php @@ -65,15 +65,11 @@ public function getVisitorsForNode(Node $node): array { $nodeClass = $node::class; - static $counter = 0; - if (! isset($this->visitorsPerNodeClass[$nodeClass])) { $this->visitorsPerNodeClass[$nodeClass] = []; /** @var RectorInterface $visitor */ foreach ($this->visitors as $visitor) { foreach ($visitor->getNodeTypes() as $nodeType) { - ++$counter; - if (is_a($nodeClass, $nodeType, true)) { $this->visitorsPerNodeClass[$nodeClass][] = $visitor; continue 2;