From c7b1c075368ff8be3f887dece078504ec1dcd86c Mon Sep 17 00:00:00 2001 From: Felix Yeung <19355619+imfelixyeung@users.noreply.github.com> Date: Wed, 26 Aug 2026 22:38:41 +0100 Subject: [PATCH 1/3] perf: implement batch formatting for supported formatters --- classes/local/cli/commands/format.php | 65 ++++++++++++++++++++++----- classes/local/format/base.php | 13 ++++++ classes/local/format/biome.php | 19 ++++++++ classes/local/format/eslint.php | 6 +++ classes/local/format/phpcbf.php | 9 ++++ classes/local/format/pint.php | 21 +++++++++ classes/local/format/stylelint.php | 6 +++ 7 files changed, 129 insertions(+), 10 deletions(-) diff --git a/classes/local/cli/commands/format.php b/classes/local/cli/commands/format.php index d1f570d9..4839440c 100644 --- a/classes/local/cli/commands/format.php +++ b/classes/local/cli/commands/format.php @@ -34,6 +34,8 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Finder\Finder; +use function count; + /** * Format command. * @@ -96,6 +98,28 @@ public function __invoke( * @param string[] $paths */ private function format_run(array $paths, ?ProgressIndicator $progress): void { + $allfiles = $this->collect_files($paths); + + $formattermap = $this->build_formatter_map($allfiles); + + foreach ($formattermap as $formatterclass => $files) { + $name = $formatterclass::get_name(); + $batches = array_chunk($files, 10); + foreach ($batches as $batch) { + $progress?->setMessage("Running $name on " . count($batch) . " files..."); + $formatterclass::format_batch($batch); + } + } + } + + /** + * Collect all files from the given paths. + * @param string[] $paths + * @return string[] + */ + private function collect_files(array $paths): array { + $files = []; + foreach ($paths as $path) { if (!file_exists($path)) { continue; @@ -125,26 +149,47 @@ private function format_run(array $paths, ?ProgressIndicator $progress): void { foreach ($finder as $file) { $realpath = $file->getRealPath(); if ($realpath !== false) { - $this->format_file($realpath, $progress); + $files[] = $realpath; } } } else { - $this->format_file($path, $progress); + $files[] = $path; } } + + return $files; } /** - * Run formatters on a single file. + * Build a map of formatter class to file paths, preserving pick_formatters order. + * + * Formatters are emitted in the order they first appear across all files, + * which respects the per-file ordering defined by pick_formatters. + * + * @param string[] $files + * @return array, string[]> */ - private function format_file(string $path, ?ProgressIndicator $progress): void { - $progress?->setMessage("Formatting $path..."); - $formatters = $this->pick_formatters($path); - foreach ($formatters as $formatter) { - $name = $formatter::get_name(); - $progress?->setMessage("Formatting $path with $name"); - $formatter::format($path); + private function build_formatter_map(array $files): array { + $formatterorder = []; + $formattermap = []; + + foreach ($files as $file) { + foreach ($this->pick_formatters($file) as $formatter) { + $class = $formatter::class; + if (!isset($formattermap[$class])) { + $formatterorder[] = $class; + $formattermap[$class] = []; + } + $formattermap[$class][] = $file; + } + } + + $ordered = []; + foreach ($formatterorder as $class) { + $ordered[$class] = $formattermap[$class]; } + + return $ordered; } /** diff --git a/classes/local/format/base.php b/classes/local/format/base.php index 1b3f7154..763d6a23 100644 --- a/classes/local/format/base.php +++ b/classes/local/format/base.php @@ -35,4 +35,17 @@ abstract public static function get_name(): string; * Apply formatting to a given file. */ abstract public static function format(string $file): ?int; + + /** + * Apply formatting to a batch of files. + * + * Subclasses may override this to pass multiple files in a single process invocation. + * + * @param string[] $files + */ + public static function format_batch(array $files): void { + foreach ($files as $file) { + static::format($file); + } + } } diff --git a/classes/local/format/biome.php b/classes/local/format/biome.php index d0a1ade0..3eeaa3d0 100644 --- a/classes/local/format/biome.php +++ b/classes/local/format/biome.php @@ -58,6 +58,25 @@ public static function format(string $path): ?int { return $process->getExitCode(); } + #[\Override] + public static function format_batch(array $files): void { + $config = self::get_config_path(); + if ($config === null) { + return; + } + + $process = new Process(array_merge([ + 'bunx', + '--bun', + '@biomejs/biome', + 'format', + '--config-path', + $config, + '--write', + ], $files)); + $process->run(); + } + /** * Get the biome.jsonc file. */ diff --git a/classes/local/format/eslint.php b/classes/local/format/eslint.php index dc927c54..a0aceac5 100644 --- a/classes/local/format/eslint.php +++ b/classes/local/format/eslint.php @@ -40,4 +40,10 @@ public static function format(string $path): ?int { return $process->getExitCode(); } + + #[\Override] + public static function format_batch(array $files): void { + $process = new Process(array_merge(['bunx', 'eslint', '--fix'], $files)); + $process->run(); + } } diff --git a/classes/local/format/phpcbf.php b/classes/local/format/phpcbf.php index 5d6395f0..c83b8f60 100644 --- a/classes/local/format/phpcbf.php +++ b/classes/local/format/phpcbf.php @@ -42,4 +42,13 @@ public static function format(string $path): ?int { return $process->getExitCode(); } + + #[\Override] + public static function format_batch(array $files): void { + $process = new Process(array_merge([ + 'phpcbf', + '-q', + ], $files), timeout: MINSECS); + $process->run(); + } } diff --git a/classes/local/format/pint.php b/classes/local/format/pint.php index 0d08bddf..0214ed69 100644 --- a/classes/local/format/pint.php +++ b/classes/local/format/pint.php @@ -54,4 +54,25 @@ public static function format(string $path): ?int { return $process->getExitCode(); } + + #[\Override] + public static function format_batch(array $files): void { + global $CFG; + $bin = realpath("$CFG->dirroot/local/devkit/vendor/bin/pint"); + $config = realpath("$CFG->dirroot/local/devkit/pint.json"); + + if ($bin === false || $config === false) { + return; + } + + $process = new Process(array_merge([ + 'php', + $bin, + '--no-interaction', + '--quiet', + '--config', + $config, + ], $files), timeout: MINSECS); + $process->run(); + } } diff --git a/classes/local/format/stylelint.php b/classes/local/format/stylelint.php index a8bc7df1..e6dc4efe 100644 --- a/classes/local/format/stylelint.php +++ b/classes/local/format/stylelint.php @@ -40,4 +40,10 @@ public static function format(string $path): ?int { return $process->getExitCode(); } + + #[\Override] + public static function format_batch(array $files): void { + $process = new Process(array_merge(['bunx', 'stylelint', '--fix'], $files)); + $process->run(); + } } From 9764e56a898ae30f82f94acc4324c46822f14f63 Mon Sep 17 00:00:00 2001 From: Felix Yeung <19355619+imfelixyeung@users.noreply.github.com> Date: Wed, 26 Aug 2026 22:43:24 +0100 Subject: [PATCH 2/3] feat: add --batch-size flag --- classes/local/cli/commands/format.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/classes/local/cli/commands/format.php b/classes/local/cli/commands/format.php index 4839440c..06871c46 100644 --- a/classes/local/cli/commands/format.php +++ b/classes/local/cli/commands/format.php @@ -29,6 +29,7 @@ use Symfony\Component\Console\Helper\ProgressIndicator; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; @@ -71,6 +72,7 @@ class format extends Command { */ protected function configure(): void { $this->addArgument('paths', InputArgument::IS_ARRAY); + $this->addOption('batch-size', null, InputOption::VALUE_REQUIRED, 'Number of files per formatter batch', 50); } /** @@ -82,12 +84,13 @@ public function __invoke( OutputInterface $output, ): int { $paths = $input->getArgument('paths'); + $batchsize = (int) $input->getOption('batch-size'); $progress = $output instanceof ConsoleOutputInterface ? new ProgressIndicator($output->getErrorOutput()) : null; $progress?->start('Starting...'); - $this->format_run($paths, $progress); + $this->format_run($paths, $batchsize, $progress); $progress?->finish('All done.'); return Command::SUCCESS; @@ -97,14 +100,14 @@ public function __invoke( * Format files in the given paths. * @param string[] $paths */ - private function format_run(array $paths, ?ProgressIndicator $progress): void { + private function format_run(array $paths, int $batchsize, ?ProgressIndicator $progress): void { $allfiles = $this->collect_files($paths); $formattermap = $this->build_formatter_map($allfiles); foreach ($formattermap as $formatterclass => $files) { $name = $formatterclass::get_name(); - $batches = array_chunk($files, 10); + $batches = array_chunk($files, $batchsize); foreach ($batches as $batch) { $progress?->setMessage("Running $name on " . count($batch) . " files..."); $formatterclass::format_batch($batch); From e52392cb8421fe2745a7efec26cc75895ecbfac1 Mon Sep 17 00:00:00 2001 From: Felix Yeung <19355619+imfelixyeung@users.noreply.github.com> Date: Wed, 26 Aug 2026 22:59:51 +0100 Subject: [PATCH 3/3] fix: validate batch-size option to ensure it is a positive integer --- classes/local/cli/commands/format.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/classes/local/cli/commands/format.php b/classes/local/cli/commands/format.php index 06871c46..86b83670 100644 --- a/classes/local/cli/commands/format.php +++ b/classes/local/cli/commands/format.php @@ -17,6 +17,7 @@ namespace local_devkit\local\cli\commands; use core\di; +use InvalidArgumentException; use local_devkit\local\format\base; use local_devkit\local\format\biome; use local_devkit\local\format\eslint; @@ -84,7 +85,12 @@ public function __invoke( OutputInterface $output, ): int { $paths = $input->getArgument('paths'); - $batchsize = (int) $input->getOption('batch-size'); + $rawbatchsize = $input->getOption('batch-size'); + if (!is_numeric($rawbatchsize) || (int) $rawbatchsize <= 0) { + $io->error('The batch-size option must be a positive integer.'); + return Command::FAILURE; + } + $batchsize = (int) $rawbatchsize; $progress = $output instanceof ConsoleOutputInterface ? new ProgressIndicator($output->getErrorOutput()) : null; @@ -101,6 +107,10 @@ public function __invoke( * @param string[] $paths */ private function format_run(array $paths, int $batchsize, ?ProgressIndicator $progress): void { + if ($batchsize < 1) { + throw new InvalidArgumentException('Argument $batchsize must be a positive integer.'); + } + $allfiles = $this->collect_files($paths); $formattermap = $this->build_formatter_map($allfiles);