From 0558d3d43abc6fdbd9acf311379dfe00ebfda783 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 18 Jun 2025 11:19:45 +0200 Subject: [PATCH 1/6] Add Linter command. --- src/Application.php | 2 + src/Command/Helper/ProgressHelper.php | 163 ++++++++++++++++++++++++++ src/Command/LinterCommand.php | 128 ++++++++++++++++++++ 3 files changed, 293 insertions(+) create mode 100644 src/Command/Helper/ProgressHelper.php create mode 100644 src/Command/LinterCommand.php diff --git a/src/Application.php b/src/Application.php index f60c7486..5430ac81 100644 --- a/src/Application.php +++ b/src/Application.php @@ -19,6 +19,7 @@ use Cake\Console\CommandCollection; use Cake\Core\ConsoleApplicationInterface; use Cake\Upgrade\Command\FileRenameCommand; +use Cake\Upgrade\Command\LinterCommand; use Cake\Upgrade\Command\RectorCommand; use Cake\Upgrade\Command\UpgradeCommand; @@ -51,6 +52,7 @@ public function console(CommandCollection $commands): CommandCollection $commands->add('upgrade', UpgradeCommand::class); $commands->add('upgrade file_rename', FileRenameCommand::class); $commands->add('upgrade rector', RectorCommand::class); + $commands->add('linter', LinterCommand::class); return $commands; } diff --git a/src/Command/Helper/ProgressHelper.php b/src/Command/Helper/ProgressHelper.php new file mode 100644 index 00000000..2c8b5ab6 --- /dev/null +++ b/src/Command/Helper/ProgressHelper.php @@ -0,0 +1,163 @@ +helper('Progress')->output(['callback' => function ($progress) { + * // Do work + * $progress->increment(); + * }); + * ``` + */ +class ProgressHelper extends Helper +{ + /** + * Default value for progress bar total value. + * Percent completion is derived from progress/total + */ + protected const DEFAULT_TOTAL = 100; + + /** + * Default value for progress bar width + */ + protected const DEFAULT_WIDTH = 80; + + /** + * The current progress. + * + * @var float|int + */ + protected float|int $_progress = 0; + + /** + * The total number of 'items' to progress through. + * + * @var int + */ + protected int $_total = self::DEFAULT_TOTAL; + + /** + * The width of the bar. + * + * @var int + */ + protected int $_width = self::DEFAULT_WIDTH; + + /** + * Output a progress bar. + * + * Takes a number of options to customize the behavior: + * + * - `total` The total number of items in the progress bar. Defaults + * to 100. + * - `width` The width of the progress bar. Defaults to 80. + * - `callback` The callback that will be called in a loop to advance the progress bar. + * + * @param array $args The arguments/options to use when outputting the progress bar. + * @return void + */ + public function output(array $args): void + { + $args += ['callback' => null]; + if (isset($args[0])) { + $args['callback'] = $args[0]; + } + if (!$args['callback'] || !is_callable($args['callback'])) { + throw new InvalidArgumentException('Callback option must be a callable.'); + } + $this->init($args); + + $callback = $args['callback']; + + $this->_io->out('', 0); + while ($this->_progress < $this->_total) { + $callback($this); + $this->draw(); + } + $this->_io->out(''); + } + + /** + * Initialize the progress bar for use. + * + * - `total` The total number of items in the progress bar. Defaults + * to 100. + * - `width` The width of the progress bar. Defaults to 80. + * + * @param array $args The initialization data. + * @return $this + */ + public function init(array $args = []) + { + $args += ['total' => self::DEFAULT_TOTAL, 'width' => self::DEFAULT_WIDTH]; + $this->_progress = 0; + $this->_width = $args['width']; + $this->_total = $args['total']; + + return $this; + } + + /** + * Increment the progress bar. + * + * @param float|int $num The amount of progress to advance by. + * @return $this + */ + public function increment(float|int $num = 1) + { + $this->_progress = min(max(0, $this->_progress + $num), $this->_total); + + return $this; + } + + /** + * Render the progress bar based on the current state. + * + * @return $this + */ + public function draw() + { + $numberLen = strlen(' 100%'); + $complete = round($this->_progress / $this->_total, 2); + $barLen = ($this->_width - $numberLen) * $this->_progress / $this->_total; + $bar = ''; + if ($barLen > 1) { + $bar = str_repeat('=', (int)$barLen - 1) . '>'; + } + + $pad = ceil($this->_width - $numberLen - $barLen); + if ($pad > 0) { + $bar .= str_repeat(' ', (int)$pad); + } + $percent = ($complete * 100) . '%'; + $bar .= str_pad($percent, $numberLen, ' ', STR_PAD_LEFT); + + $this->_io->overwrite($bar, 0); + + return $this; + } +} diff --git a/src/Command/LinterCommand.php b/src/Command/LinterCommand.php new file mode 100644 index 00000000..1cfa149d --- /dev/null +++ b/src/Command/LinterCommand.php @@ -0,0 +1,128 @@ +addArgument('path', [ + 'help' => $help, + 'default' => null, + 'required' => false, + ]) + ->setDescription(static::getDescription()); + } + + /** + * Implement this method with your command's logic. + * + * @param \Cake\Console\Arguments $args The command arguments. + * @param \Cake\Console\ConsoleIo $io The console io + * @return int The exit code + */ + public function execute(Arguments $args, ConsoleIo $io): int + { + $directories = $args->getArgument('path') ?: static::$defaultDirectories; + if (is_string($directories) && str_contains($directories, ',')) { + $directories = explode(',', $directories); + } + + $result = static::CODE_SUCCESS; + foreach ((array)$directories as $directory) { + if (!file_exists($directory)) { + $io->warning('Not exists: ' . $directory . ' - skipping.'); + + continue; + } + + $io->out('Checking ' . (is_file($directory) ? 'file' : 'directory') . ': ' . $directory); + if (is_file($directory)) { + $phpFiles = [ + new SplFileInfo($directory), + ]; + } else { + $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)); + $phpFiles = new RegexIterator($files, '/\.php$/'); + } + + $fileCount = iterator_count($phpFiles); + $io->helper('Progress')->init(['total' => $fileCount]); + + /** @var \SplFileInfo $file */ + foreach ($phpFiles as $file) { + $io->verbose('Checking ' . $file->getPathname()); + + exec('php -l ' . escapeshellarg($file->getPathname()), $output, $returnVar); + if ($returnVar !== 0) { + $io->err('Error in ' . $file->getPathname() . ': ' . implode("\n", $output)); + $result = self::CODE_ERROR; + + continue; + } + + $io->helper('Progress')->increment(); + $io->helper('Progress')->draw(); + } + + $io->out(''); + } + + if ($result === self::CODE_SUCCESS) { + $io->success('All files are valid.'); + } else { + $io->error('Some files have errors. Please check the output above.'); + } + + return $result; + } +} From 47ca9894422fd7a01fa9403d65dd64d2153ae57a Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 18 Jun 2025 11:24:26 +0200 Subject: [PATCH 2/6] Add Linter command. --- src/Command/LinterCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Command/LinterCommand.php b/src/Command/LinterCommand.php index 1cfa149d..85278b86 100644 --- a/src/Command/LinterCommand.php +++ b/src/Command/LinterCommand.php @@ -97,6 +97,7 @@ public function execute(Arguments $args, ConsoleIo $io): int $fileCount = iterator_count($phpFiles); $io->helper('Progress')->init(['total' => $fileCount]); + $io->out('', 0); /** @var \SplFileInfo $file */ foreach ($phpFiles as $file) { From e75a9a19fdbd3fe8d9bfb989d81b814c7d443194 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 18 Jun 2025 11:31:04 +0200 Subject: [PATCH 3/6] Add test case. --- src/Command/LinterCommand.php | 20 +++++++- tests/TestCase/Command/LinterCommandTest.php | 48 ++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 tests/TestCase/Command/LinterCommandTest.php diff --git a/src/Command/LinterCommand.php b/src/Command/LinterCommand.php index 85278b86..1872f413 100644 --- a/src/Command/LinterCommand.php +++ b/src/Command/LinterCommand.php @@ -47,7 +47,7 @@ public static function getDescription(): string public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $dirs = []; - foreach (static::$defaultDirectories as $dir) { + foreach ($this->defaultDirectories() as $dir) { $dirs[] = '`' . $dir . '`'; } @@ -72,7 +72,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar */ public function execute(Arguments $args, ConsoleIo $io): int { - $directories = $args->getArgument('path') ?: static::$defaultDirectories; + $directories = $args->getArgument('path') ?: $this->defaultDirectories(); if (is_string($directories) && str_contains($directories, ',')) { $directories = explode(',', $directories); } @@ -126,4 +126,20 @@ public function execute(Arguments $args, ConsoleIo $io): int return $result; } + + /** + * @return array + */ + protected function defaultDirectories(): array + { + $result = []; + foreach (static::$defaultDirectories as $directory) { + if (!is_dir($directory)) { + continue; + } + $result[] = $directory; + } + + return $result; + } } diff --git a/tests/TestCase/Command/LinterCommandTest.php b/tests/TestCase/Command/LinterCommandTest.php new file mode 100644 index 00000000..fb094db0 --- /dev/null +++ b/tests/TestCase/Command/LinterCommandTest.php @@ -0,0 +1,48 @@ +configApplication('\Cake\Upgrade\Application', []); + } + + /** + * @return void + */ + public function testLinter() + { + $this->exec('linter'); + + $this->assertExitSuccess(); + $this->assertOutputContains('All files are valid.'); + } +} From 09c311e3808177e0ae1afc70768969d4d0bec480 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 18 Jun 2025 12:40:16 +0200 Subject: [PATCH 4/6] Refactor to symfony command. --- src/Command/LinterCommand.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Command/LinterCommand.php b/src/Command/LinterCommand.php index 1872f413..aabcf0a8 100644 --- a/src/Command/LinterCommand.php +++ b/src/Command/LinterCommand.php @@ -11,6 +11,7 @@ use RecursiveIteratorIterator; use RegexIterator; use SplFileInfo; +use Symfony\Component\Process\Process; class LinterCommand extends BaseCommand { @@ -78,6 +79,7 @@ public function execute(Arguments $args, ConsoleIo $io): int } $result = static::CODE_SUCCESS; + $errors = []; foreach ((array)$directories as $directory) { if (!file_exists($directory)) { $io->warning('Not exists: ' . $directory . ' - skipping.'); @@ -103,9 +105,12 @@ public function execute(Arguments $args, ConsoleIo $io): int foreach ($phpFiles as $file) { $io->verbose('Checking ' . $file->getPathname()); - exec('php -l ' . escapeshellarg($file->getPathname()), $output, $returnVar); - if ($returnVar !== 0) { - $io->err('Error in ' . $file->getPathname() . ': ' . implode("\n", $output)); + $command = 'php -l ' . $file->getPathname(); + $process = Process::fromShellCommandline($command); + $process->run(); + + if ($process->getExitCode() !== 0) { + $errors[] = $file->getPathname() . ': ' . $process->getErrorOutput(); $result = self::CODE_ERROR; continue; @@ -118,6 +123,10 @@ public function execute(Arguments $args, ConsoleIo $io): int $io->out(''); } + if ($errors) { + $io->err($errors); + } + if ($result === self::CODE_SUCCESS) { $io->success('All files are valid.'); } else { From 2b8fa306895b495d37d2acee453e551c43140c34 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Wed, 18 Jun 2025 13:34:25 +0200 Subject: [PATCH 5/6] Update src/Command/LinterCommand.php Co-authored-by: ADmad --- src/Command/LinterCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Command/LinterCommand.php b/src/Command/LinterCommand.php index aabcf0a8..4822b1cf 100644 --- a/src/Command/LinterCommand.php +++ b/src/Command/LinterCommand.php @@ -82,7 +82,7 @@ public function execute(Arguments $args, ConsoleIo $io): int $errors = []; foreach ((array)$directories as $directory) { if (!file_exists($directory)) { - $io->warning('Not exists: ' . $directory . ' - skipping.'); + $io->warning('Does not exist: ' . $directory . ' - skipping.'); continue; } From 1b49a02df5e820f8a623a989a621543f68a5c35a Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 18 Jun 2025 13:43:41 +0200 Subject: [PATCH 6/6] add false test. --- src/Command/LinterCommand.php | 2 +- tests/TestCase/Command/LinterCommandTest.php | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Command/LinterCommand.php b/src/Command/LinterCommand.php index 4822b1cf..1463172e 100644 --- a/src/Command/LinterCommand.php +++ b/src/Command/LinterCommand.php @@ -109,7 +109,7 @@ public function execute(Arguments $args, ConsoleIo $io): int $process = Process::fromShellCommandline($command); $process->run(); - if ($process->getExitCode() !== 0) { + if ($process->getExitCode() !== static::CODE_SUCCESS) { $errors[] = $file->getPathname() . ': ' . $process->getErrorOutput(); $result = self::CODE_ERROR; diff --git a/tests/TestCase/Command/LinterCommandTest.php b/tests/TestCase/Command/LinterCommandTest.php index fb094db0..91191f22 100644 --- a/tests/TestCase/Command/LinterCommandTest.php +++ b/tests/TestCase/Command/LinterCommandTest.php @@ -38,11 +38,24 @@ public function setUp(): void /** * @return void */ - public function testLinter() + public function testLinterSuccess() { $this->exec('linter'); $this->assertExitSuccess(); $this->assertOutputContains('All files are valid.'); } + + /** + * @return void + */ + public function testLinterFail() + { + mkdir(TMP . 'linter', 0777, true); + file_put_contents(TMP . 'linter/BrokenExample.php', 'exec('linter tmp/linter/ -v'); + + $this->assertExitError(); + $this->assertErrorContains('Some files have errors.'); + } }