From c8c7e93c6002943b0b613fd7ab694fa134b5d263 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 23 Oct 2026 21:41:36 +0200 Subject: [PATCH 1/2] [Application] Skip format-preserving print for unchanged files --- bench/skip_unchanged_file_print_bench.php | 92 +++++++++++++++++++++++ src/Application/FileProcessor.php | 11 +++ 2 files changed, 103 insertions(+) create mode 100644 bench/skip_unchanged_file_print_bench.php diff --git a/bench/skip_unchanged_file_print_bench.php b/bench/skip_unchanged_file_print_bench.php new file mode 100644 index 00000000000..5c4761b41ee --- /dev/null +++ b/bench/skip_unchanged_file_print_bench.php @@ -0,0 +1,92 @@ +create(); +$rectorConfig->boot(); + +$fileProcessor = $rectorConfig->make(FileProcessor::class); + +/** @var CurrentFileProvider $currentFileProvider */ +$currentFileProvider = $rectorConfig->make(CurrentFileProvider::class); + +/** @var DynamicSourceLocatorProvider $dynamicSourceLocatorProvider */ +$dynamicSourceLocatorProvider = $rectorConfig->make(DynamicSourceLocatorProvider::class); + +/** @var ConfigurationFactory $configurationFactory */ +$configurationFactory = $rectorConfig->make(ConfigurationFactory::class); + +// build a sizable, already-clean file; no rule is registered so it stays unchanged +$source = "setFilePath($sourceFilePath); +$configuration = $configurationFactory->createForTests([$sourceFilePath]); + +$cpuSeconds = static function (): float { + $usage = getrusage(); + return $usage['ru_utime.tv_sec'] + $usage['ru_utime.tv_usec'] / 1_000_000 + + $usage['ru_stime.tv_sec'] + $usage['ru_stime.tv_usec'] / 1_000_000; +}; + +$run = static function () use ($fileProcessor, $currentFileProvider, $sourceFilePath, $configuration): void { + // File is single-use (hydrateStmtsAndTokens rejects a second hydrate), so make a fresh one + $file = new File($sourceFilePath, file_get_contents($sourceFilePath)); + $currentFileProvider->setFile($file); + $fileProcessor->processFile($file, $configuration); +}; + +// warm up autoloading/reflection/scope caches once, excluded from timing +$run(); + +$wallStart = microtime(true); +$cpuStart = $cpuSeconds(); + +for ($repeat = 0; $repeat < REPEAT; ++$repeat) { + $run(); +} + +$cpu = $cpuSeconds() - $cpuStart; +$wall = microtime(true) - $wallStart; + +@unlink($sourceFilePath); + +printf('statements: %d, files processed: %d%s', STATEMENTS, REPEAT, PHP_EOL); +printf('CPU: %.4f s (%.3f ms/file)%s', $cpu, $cpu / REPEAT * 1_000, PHP_EOL); +printf('wall: %.4f s%s', $wall, PHP_EOL); diff --git a/src/Application/FileProcessor.php b/src/Application/FileProcessor.php index 4385f4aceec..d98296ed21c 100644 --- a/src/Application/FileProcessor.php +++ b/src/Application/FileProcessor.php @@ -69,6 +69,9 @@ public function processFile(File $file, Configuration $configuration): FileProce do { $file->changeHasChanged(false); + // every rule and post rector records a line change when it mutates the AST + $changeCountBeforePass = count($file->getRectorWithLineChanges()); + // 1. change nodes with Rector Rules $newStmts = $this->rectorNodeTraverser->traverse($file->getNewStmts(), $file); @@ -78,6 +81,14 @@ public function processFile(File $file, Configuration $configuration): FileProce // 3. this is needed for new tokens added in "afterTraverse()" $file->changeNewStmts($postNewStmts); + // file never changed and this pass changed nothing: the print would just reproduce the + // original content, so skip the expensive format-preserving print entirely. + // once the file has changed we keep printing every pass, as a reprint can still + // stabilize the output (e.g. drop a leftover trailing comma) + if (! $fileHasChanged && count($file->getRectorWithLineChanges()) === $changeCountBeforePass) { + break; + } + // 4. print to file or string // important to detect if file has changed $this->printFile($file, $configuration, $filePath); From 8b4c7e97291e3dfbe1e5c95fa9a1839fb61eefab Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 23 Oct 2026 23:11:29 +0200 Subject: [PATCH 2/2] [Removing][CodeQuality] Record change when mutating in place, so skipped-print engine detects it --- .../InlineConstructorDefaultToPropertyRector.php | 4 ++-- .../Rector/ClassMethod/ArgumentRemoverRector.php | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/rules/CodeQuality/Rector/Class_/InlineConstructorDefaultToPropertyRector.php b/rules/CodeQuality/Rector/Class_/InlineConstructorDefaultToPropertyRector.php index 0da8c402beb..6963c708094 100644 --- a/rules/CodeQuality/Rector/Class_/InlineConstructorDefaultToPropertyRector.php +++ b/rules/CodeQuality/Rector/Class_/InlineConstructorDefaultToPropertyRector.php @@ -101,9 +101,9 @@ public function refactor(Node $node): ?Node $hasParentConstructCall = false; foreach ($constructClassMethod->stmts as $key => $stmt) { - // code that is possibly breaking flow + // code that is possibly breaking flow; keep any inlining already done before it if ($stmt instanceof If_) { - return null; + break; } if (! $stmt instanceof Expression) { diff --git a/rules/Removing/Rector/ClassMethod/ArgumentRemoverRector.php b/rules/Removing/Rector/ClassMethod/ArgumentRemoverRector.php index 99b4fe9e77f..5eff096509b 100644 --- a/rules/Removing/Rector/ClassMethod/ArgumentRemoverRector.php +++ b/rules/Removing/Rector/ClassMethod/ArgumentRemoverRector.php @@ -115,9 +115,13 @@ private function processPosition( ): void { if ($argumentRemover->getValue() === null) { if ($node instanceof MethodCall || $node instanceof StaticCall) { - unset($node->args[$argumentRemover->getPosition()]); - } else { + if (isset($node->args[$argumentRemover->getPosition()])) { + unset($node->args[$argumentRemover->getPosition()]); + $this->hasChanged = true; + } + } elseif (isset($node->params[$argumentRemover->getPosition()])) { unset($node->params[$argumentRemover->getPosition()]); + $this->hasChanged = true; } return; @@ -149,6 +153,7 @@ private function removeByName(ClassMethod|StaticCall|MethodCall $node, int $posi if ($node instanceof MethodCall || $node instanceof StaticCall) { if (isset($node->args[$position]) && $this->isName($node->args[$position], $name)) { unset($node->args[$position]); + $this->hasChanged = true; } return; @@ -159,6 +164,7 @@ private function removeByName(ClassMethod|StaticCall|MethodCall $node, int $posi } unset($node->params[$position]); + $this->hasChanged = true; } /**