Skip to content

Commit 69fea8b

Browse files
dereuromarkADmad
authored andcommitted
Add Linter command. (#320)
* Add Linter command. * Add Linter command. * Add test case. * Refactor to symfony command. * Update src/Command/LinterCommand.php Co-authored-by: ADmad <ADmad@users.noreply.github.com> * add false test. --------- Co-authored-by: ADmad <ADmad@users.noreply.github.com>
1 parent a13b9dc commit 69fea8b

File tree

4 files changed

+380
-0
lines changed

4 files changed

+380
-0
lines changed

src/Application.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Cake\Console\CommandCollection;
2020
use Cake\Core\ConsoleApplicationInterface;
2121
use Cake\Upgrade\Command\FileRenameCommand;
22+
use Cake\Upgrade\Command\LinterCommand;
2223
use Cake\Upgrade\Command\RectorCommand;
2324
use Cake\Upgrade\Command\UpgradeCommand;
2425

@@ -51,6 +52,7 @@ public function console(CommandCollection $commands): CommandCollection
5152
$commands->add('upgrade', UpgradeCommand::class);
5253
$commands->add('upgrade file_rename', FileRenameCommand::class);
5354
$commands->add('upgrade rector', RectorCommand::class);
55+
$commands->add('linter', LinterCommand::class);
5456

5557
return $commands;
5658
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
6+
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
7+
*
8+
* Licensed under The MIT License
9+
* For full copyright and license information, please see the LICENSE.txt
10+
* Redistributions of files must retain the above copyright notice.
11+
*
12+
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
13+
* @link https://cakephp.org CakePHP Project
14+
* @since 3.1.0
15+
* @license https://opensource.org/licenses/mit-license.php MIT License
16+
*/
17+
namespace Cake\Upgrade\Command\Helper;
18+
19+
use Cake\Console\Helper;
20+
use InvalidArgumentException;
21+
22+
/**
23+
* Create a progress bar using a supplied callback.
24+
*
25+
* ## Usage
26+
*
27+
* The ProgressHelper can be accessed from shells using the helper() method
28+
*
29+
* ```
30+
* $this->helper('Progress')->output(['callback' => function ($progress) {
31+
* // Do work
32+
* $progress->increment();
33+
* });
34+
* ```
35+
*/
36+
class ProgressHelper extends Helper
37+
{
38+
/**
39+
* Default value for progress bar total value.
40+
* Percent completion is derived from progress/total
41+
*/
42+
protected const DEFAULT_TOTAL = 100;
43+
44+
/**
45+
* Default value for progress bar width
46+
*/
47+
protected const DEFAULT_WIDTH = 80;
48+
49+
/**
50+
* The current progress.
51+
*
52+
* @var float|int
53+
*/
54+
protected float|int $_progress = 0;
55+
56+
/**
57+
* The total number of 'items' to progress through.
58+
*
59+
* @var int
60+
*/
61+
protected int $_total = self::DEFAULT_TOTAL;
62+
63+
/**
64+
* The width of the bar.
65+
*
66+
* @var int
67+
*/
68+
protected int $_width = self::DEFAULT_WIDTH;
69+
70+
/**
71+
* Output a progress bar.
72+
*
73+
* Takes a number of options to customize the behavior:
74+
*
75+
* - `total` The total number of items in the progress bar. Defaults
76+
* to 100.
77+
* - `width` The width of the progress bar. Defaults to 80.
78+
* - `callback` The callback that will be called in a loop to advance the progress bar.
79+
*
80+
* @param array $args The arguments/options to use when outputting the progress bar.
81+
* @return void
82+
*/
83+
public function output(array $args): void
84+
{
85+
$args += ['callback' => null];
86+
if (isset($args[0])) {
87+
$args['callback'] = $args[0];
88+
}
89+
if (!$args['callback'] || !is_callable($args['callback'])) {
90+
throw new InvalidArgumentException('Callback option must be a callable.');
91+
}
92+
$this->init($args);
93+
94+
$callback = $args['callback'];
95+
96+
$this->_io->out('', 0);
97+
while ($this->_progress < $this->_total) {
98+
$callback($this);
99+
$this->draw();
100+
}
101+
$this->_io->out('');
102+
}
103+
104+
/**
105+
* Initialize the progress bar for use.
106+
*
107+
* - `total` The total number of items in the progress bar. Defaults
108+
* to 100.
109+
* - `width` The width of the progress bar. Defaults to 80.
110+
*
111+
* @param array<string, mixed> $args The initialization data.
112+
* @return $this
113+
*/
114+
public function init(array $args = [])
115+
{
116+
$args += ['total' => self::DEFAULT_TOTAL, 'width' => self::DEFAULT_WIDTH];
117+
$this->_progress = 0;
118+
$this->_width = $args['width'];
119+
$this->_total = $args['total'];
120+
121+
return $this;
122+
}
123+
124+
/**
125+
* Increment the progress bar.
126+
*
127+
* @param float|int $num The amount of progress to advance by.
128+
* @return $this
129+
*/
130+
public function increment(float|int $num = 1)
131+
{
132+
$this->_progress = min(max(0, $this->_progress + $num), $this->_total);
133+
134+
return $this;
135+
}
136+
137+
/**
138+
* Render the progress bar based on the current state.
139+
*
140+
* @return $this
141+
*/
142+
public function draw()
143+
{
144+
$numberLen = strlen(' 100%');
145+
$complete = round($this->_progress / $this->_total, 2);
146+
$barLen = ($this->_width - $numberLen) * $this->_progress / $this->_total;
147+
$bar = '';
148+
if ($barLen > 1) {
149+
$bar = str_repeat('=', (int)$barLen - 1) . '>';
150+
}
151+
152+
$pad = ceil($this->_width - $numberLen - $barLen);
153+
if ($pad > 0) {
154+
$bar .= str_repeat(' ', (int)$pad);
155+
}
156+
$percent = ($complete * 100) . '%';
157+
$bar .= str_pad($percent, $numberLen, ' ', STR_PAD_LEFT);
158+
159+
$this->_io->overwrite($bar, 0);
160+
161+
return $this;
162+
}
163+
}

src/Command/LinterCommand.php

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Cake\Upgrade\Command;
5+
6+
use Cake\Console\Arguments;
7+
use Cake\Console\BaseCommand;
8+
use Cake\Console\ConsoleIo;
9+
use Cake\Console\ConsoleOptionParser;
10+
use RecursiveDirectoryIterator;
11+
use RecursiveIteratorIterator;
12+
use RegexIterator;
13+
use SplFileInfo;
14+
use Symfony\Component\Process\Process;
15+
16+
class LinterCommand extends BaseCommand
17+
{
18+
protected static array $defaultDirectories = [
19+
'src/',
20+
'config/',
21+
'templates/',
22+
'tests/',
23+
];
24+
25+
/**
26+
* The name of this command.
27+
*
28+
* @var string
29+
*/
30+
protected string $name = 'linter';
31+
32+
/**
33+
* Get the command description.
34+
*
35+
* @return string
36+
*/
37+
public static function getDescription(): string
38+
{
39+
return 'Check PHP files using PHP linter.';
40+
}
41+
42+
/**
43+
* Hook method for defining this command's option parser.
44+
*
45+
* @param \Cake\Console\ConsoleOptionParser $parser The parser to be defined
46+
* @return \Cake\Console\ConsoleOptionParser The built parser.
47+
*/
48+
public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
49+
{
50+
$dirs = [];
51+
foreach ($this->defaultDirectories() as $dir) {
52+
$dirs[] = '`' . $dir . '`';
53+
}
54+
55+
$help = 'The path (or comma separated paths) to the file or directory to check.';
56+
$help .= ' If not provided, defaults to checking common directories ' . implode(', ', $dirs) . '.';
57+
58+
return parent::buildOptionParser($parser)
59+
->addArgument('path', [
60+
'help' => $help,
61+
'default' => null,
62+
'required' => false,
63+
])
64+
->setDescription(static::getDescription());
65+
}
66+
67+
/**
68+
* Implement this method with your command's logic.
69+
*
70+
* @param \Cake\Console\Arguments $args The command arguments.
71+
* @param \Cake\Console\ConsoleIo $io The console io
72+
* @return int The exit code
73+
*/
74+
public function execute(Arguments $args, ConsoleIo $io): int
75+
{
76+
$directories = $args->getArgument('path') ?: $this->defaultDirectories();
77+
if (is_string($directories) && str_contains($directories, ',')) {
78+
$directories = explode(',', $directories);
79+
}
80+
81+
$result = static::CODE_SUCCESS;
82+
$errors = [];
83+
foreach ((array)$directories as $directory) {
84+
if (!file_exists($directory)) {
85+
$io->warning('Does not exist: ' . $directory . ' - skipping.');
86+
87+
continue;
88+
}
89+
90+
$io->out('Checking ' . (is_file($directory) ? 'file' : 'directory') . ': ' . $directory);
91+
if (is_file($directory)) {
92+
$phpFiles = [
93+
new SplFileInfo($directory),
94+
];
95+
} else {
96+
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
97+
$phpFiles = new RegexIterator($files, '/\.php$/');
98+
}
99+
100+
$fileCount = iterator_count($phpFiles);
101+
$io->helper('Progress')->init(['total' => $fileCount]);
102+
$io->out('', 0);
103+
104+
/** @var \SplFileInfo $file */
105+
foreach ($phpFiles as $file) {
106+
$io->verbose('Checking ' . $file->getPathname());
107+
108+
$command = 'php -l ' . $file->getPathname();
109+
$process = Process::fromShellCommandline($command);
110+
$process->run();
111+
112+
if ($process->getExitCode() !== static::CODE_SUCCESS) {
113+
$errors[] = $file->getPathname() . ': ' . $process->getErrorOutput();
114+
$result = self::CODE_ERROR;
115+
116+
continue;
117+
}
118+
119+
$io->helper('Progress')->increment();
120+
$io->helper('Progress')->draw();
121+
}
122+
123+
$io->out('');
124+
}
125+
126+
if ($errors) {
127+
$io->err($errors);
128+
}
129+
130+
if ($result === self::CODE_SUCCESS) {
131+
$io->success('All files are valid.');
132+
} else {
133+
$io->error('Some files have errors. Please check the output above.');
134+
}
135+
136+
return $result;
137+
}
138+
139+
/**
140+
* @return array<string>
141+
*/
142+
protected function defaultDirectories(): array
143+
{
144+
$result = [];
145+
foreach (static::$defaultDirectories as $directory) {
146+
if (!is_dir($directory)) {
147+
continue;
148+
}
149+
$result[] = $directory;
150+
}
151+
152+
return $result;
153+
}
154+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
6+
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
7+
*
8+
* Licensed under The MIT License
9+
* For full copyright and license information, please see the LICENSE.txt
10+
* Redistributions of files must retain the above copyright notice.
11+
*
12+
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
13+
* @link https://cakephp.org CakePHP(tm) Project
14+
* @since 6.0.0
15+
* @license https://opensource.org/licenses/mit-license.php MIT License
16+
*/
17+
namespace Cake\Upgrade\Test\TestCase\Command;
18+
19+
use Cake\Console\TestSuite\ConsoleIntegrationTestTrait;
20+
use Cake\Upgrade\Test\TestCase\TestCase;
21+
22+
class LinterCommandTest extends TestCase
23+
{
24+
use ConsoleIntegrationTestTrait;
25+
26+
/**
27+
* setup method
28+
*
29+
* @return void
30+
*/
31+
public function setUp(): void
32+
{
33+
parent::setUp();
34+
35+
$this->configApplication('\Cake\Upgrade\Application', []);
36+
}
37+
38+
/**
39+
* @return void
40+
*/
41+
public function testLinterSuccess()
42+
{
43+
$this->exec('linter');
44+
45+
$this->assertExitSuccess();
46+
$this->assertOutputContains('All files are valid.');
47+
}
48+
49+
/**
50+
* @return void
51+
*/
52+
public function testLinterFail()
53+
{
54+
mkdir(TMP . 'linter', 0777, true);
55+
file_put_contents(TMP . 'linter/BrokenExample.php', '<?php class X }');
56+
$this->exec('linter tmp/linter/ -v');
57+
58+
$this->assertExitError();
59+
$this->assertErrorContains('Some files have errors.');
60+
}
61+
}

0 commit comments

Comments
 (0)