From c96ee137cc198c18f9db229c7ee7b6f6f7f7c84d Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 10 Oct 2026 15:04:15 +0200 Subject: [PATCH] Dedupe skip match resolution per file in RectorRunner --- src/Rector/RectorRunner.php | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/Rector/RectorRunner.php b/src/Rector/RectorRunner.php index e1f2cb4b872..98552202c0f 100644 --- a/src/Rector/RectorRunner.php +++ b/src/Rector/RectorRunner.php @@ -28,7 +28,7 @@ * * @see \Rector\Tests\Rector\RectorRunner\RectorRunnerTest */ -final readonly class RectorRunner +final class RectorRunner { private const string EMPTY_NODE_ARRAY_MESSAGE = <<refactor()" returns non-empty array for Nodes. @@ -42,11 +42,21 @@ return \PhpParser\NodeVisitor::REMOVE_NODE; CODE_SAMPLE; + /** + * Skip resolution depends only on the rule and file, not the node, so cache it per file + * to avoid re-running reflection + class-skip resolution on every node of the same rule. + * + * @var array, SkipMatch|null> + */ + private array $skipMatchByRectorClass = []; + + private ?string $skipMatchFilePath = null; + public function __construct( - private Skipper $skipper, - private CreatedByRuleDecorator $createdByRuleDecorator, - private ChangedNodeScopeRefresher $changedNodeScopeRefresher, - private CurrentFileProvider $currentFileProvider, + private readonly Skipper $skipper, + private readonly CreatedByRuleDecorator $createdByRuleDecorator, + private readonly ChangedNodeScopeRefresher $changedNodeScopeRefresher, + private readonly CurrentFileProvider $currentFileProvider, ) { } @@ -71,7 +81,7 @@ public function run(RectorInterface $rector, Node $node): int|Node|null|array // class/path skip is configured for this rule and file: run the rule on a deep clone to learn // whether it would actually have changed anything. Only a skip that prevents a real change // counts as used; the original node is left untouched, so the file stays skipped either way. - $skipMatch = $this->skipper->matchSkip($rector, $filePath); + $skipMatch = $this->matchSkipByRectorClass($rector, $filePath); if ($skipMatch instanceof SkipMatch) { if ($rector->refactor($this->cloneNode($node)) !== null) { $this->skipper->markSkipUsed($skipMatch); @@ -113,6 +123,21 @@ public function run(RectorInterface $rector, Node $node): int|Node|null|array return $this->postRefactorProcess($rector, $file, $originalNode, $node, $refactoredNodeOrState, $filePath); } + private function matchSkipByRectorClass(RectorInterface $rector, string $filePath): ?SkipMatch + { + // reset the cache when a new file is processed + if ($this->skipMatchFilePath !== $filePath) { + $this->skipMatchByRectorClass = []; + $this->skipMatchFilePath = $filePath; + } + + if (! array_key_exists($rector::class, $this->skipMatchByRectorClass)) { + $this->skipMatchByRectorClass[$rector::class] = $this->skipper->matchSkip($rector, $filePath); + } + + return $this->skipMatchByRectorClass[$rector::class]; + } + private function getFile(): File { $file = $this->currentFileProvider->getFile();