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/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; } /** diff --git a/src/Application/FileProcessor.php b/src/Application/FileProcessor.php index 91f2ec44a1c..2d65ae111ad 100644 --- a/src/Application/FileProcessor.php +++ b/src/Application/FileProcessor.php @@ -62,6 +62,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); @@ -71,6 +74,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);